Skip to content
Markdown

Agentic AI detection and response

Scope: detecting malicious behaviour in AI agents that employees and customers already run, using endpoint telemetry rather than network interception, and a cost-tiered LLM cascade rather than static rules. This page covers the sensor, the two-tier detector, the ADR-Bench evaluation set, and an audit of what the published detection numbers establish. The attack surface itself is framed in the agent threat model; the instrumentation side is agent observability; a worked incident is anatomy of an autonomous agent intrusion.

Figures below are re-derived from the ADR paper (MLSys 2026, arXiv 2605.17380) and the open-source repository at commit 73873e1 (3 August 2026). The Python block was executed with numpy. Repository commands are reference templates; the benchmark ships synthetic attack fixtures and its own README states it is not for production use.

What it is

An enterprise now runs agents it did not write: Claude Code, Cursor, Codex, Cline, and internal automation, each holding credentials and calling tools through the Model Context Protocol. Endpoint detection and response sees the consequences of that (a file write, a network call) but not the cause. It cannot see the prompt that triggered the write, the agent's reasoning about what to write, or which MCP tool produced the data. Distinguishing an agent exfiltrating credentials from an agent saving a config file needs the causal chain, not the syscall.

ADR is a production system at Uber, deployed for over ten months across corporate MacBook endpoints, reaching more than 7,200 unique hosts and processing over 10,000 agent sessions daily. It has four parts, of which three are open source:

  • Sensor. A Python library that parses each agent's own local logs and normalises them into one AgentEvent schema (session_id, timestamp, chat history, tools, model). The released package ships six parsers, one per format: Claude Code JSONL under ~/.claude/projects/, Cursor's state.vscdb SQLite, Cline JSON task files, Claude Desktop audit JSONL, Codex CLI JSONL under ~/.codex/sessions/, and Warp's warp.sqlite. The repository root advertises "7+ AI coding tools", which the shipped parser directory does not currently match.
  • Detector. A two-tier cascade. Tier 1 is a single short LLM call tuned for recall that short-circuits clearly benign sessions. Tier 2 is a reasoning agent that investigates escalated sessions with three MCP context providers: source-code inspection of the tool that was actually called, threat-intelligence lookup, and policy verification. "Cheap" here means one call against up to 60 agent turns, not a cheaper model: the shipped config prices Tier 1 (gpt-4o) at $2.50/$10.00 per million input/output tokens and Tier 2 (claude-sonnet-4-6) at $3.00/$15.00, within 1.5x of each other.
  • ADR-Bench. 303 MCP-driven business tasks (261 benign, 42 malicious) across 133 MCP servers, with the 42 attacks spanning 17 threat techniques grouped into 5 tactics, plus an AgentDojo integration for prompt injection.
  • Explorer and Prevention. Offline red teaming that evolves hard attack variants, and an inline pre-prompt hook that blocks secrets before transmission. Neither is in the open-source release.

Why use it

  • The telemetry is the point. A sensor reading agent-local logs captures prompts, reasoning, and tool calls with environmental context. The paper argues the alternative, an LLM/MCP gateway intercepting traffic at the network boundary, requires host changes, breaks on streaming responses, and still misses intent, because the MCP schema has no field carrying the originating prompt.
  • Latency, more than spend, is the reason for the cascade. Triage resolves 40.7% of tasks at $0.017 and 2.3 seconds; the escalated remainder costs $0.029 and 29.7 seconds. At the reported 10,000 sessions per day that is about $88k a year against $114k for sending everything to Tier 2, a real saving but not a feasibility threshold, while the same change removes roughly 27 detector-hours of latency per day.
  • Static rules do not generalise. A regex-and-entropy prevention hook reached 97.2% precision on credential strings (206 true positives against 6 false positives). The paper is explicit that this works for static patterns and fails for attacks needing reasoning about tool semantics and policy.
  • It is measurable. ADR-Bench is a released artifact with ground-truth labels, so a detector change can be regressed rather than argued about.

When to use it (and when not)

  • Use the sensor when you have coding agents on managed endpoints and no visibility into what they do. It is the cheapest part to adopt and useful on its own.
  • Use the cascade shape when detection cost per session is your constraint and you have a cheap model that is genuinely high-recall on your traffic. Verify that second condition before adopting the first tier.
  • Use ADR-Bench as a regression suite for your own detector, not as evidence about your environment. Its 13.9% attack rate is a benchmark construction, and the paper says so directly: it "is not intended to mirror the true incidence rates of different incident categories in a production environment".
  • Do not deploy it as a blocker. The open release is detection only; prevention is not published, and the shipped configuration misses a third of the benchmark's attacks.
  • Do not run the benchmark outside an isolated container or VM. It ships synthetic credentials, prompt-injection payloads, emulated vulnerable MCP servers, and dependencies pinned to versions with known CVEs.
  • Do not adopt it where endpoint telemetry collection is not already legally cleared. The deployment retains sensitive session telemetry for 13 months.

Architecture

flowchart TB
  subgraph EP["Endpoints"]
    A1["Claude Code JSONL"] --> P["Source-specific parsers"]
    A2["Cursor state.vscdb"] --> P
    A3["Codex CLI JSONL"] --> P
    A4["Cline / Warp / Claude Desktop"] --> P
  end
  P --> SCH["Unified AgentEvent schema"]
  SCH --> T1{"Tier 1 triage (cheap LLM, high recall)"}
  T1 -->|"clearly benign, 40.7%"| CLOSE["Auto-resolve"]
  T1 -->|"suspicious or uncertain"| T2["Tier 2 reasoning agent"]
  T2 <--> MCP1["Source-code inspection MCP"]
  T2 <--> MCP2["Threat intelligence MCP"]
  T2 <--> MCP3["Policy verification MCP"]
  T2 --> Q["Analyst review queue"]
  EXP["Offline Explorer red teaming"] -.->|"new attack patterns"| MCP2
  PREV["Pre-prompt blocking hook"] -.->|"not open source"| EP

The escalation policy is the operating point. Tier 1 decides the escalation rate, which sets cost and latency directly, and it also sets the ceiling on recall, because Tier 2 never sees what Tier 1 resolves.

How to use it

Everything in the published evaluation reduces to a cascade with two knobs and a base rate. The block below re-derives the paper's numbers from its own tables, then tests what they imply.

# cascade_audit.py: executed model of the two-tier detector and its published numbers.
import numpy as np

BENCH = dict(n=302, malicious=42, benign=260)
ADR = dict(prec=1.000, recall=0.667, f1=0.800, tp=28, fp=0, cost=0.024, lat=18.5)
NO_TRIAGE = dict(prec=0.943, recall=0.805, f1=0.868, cost=0.031, lat=28.1)
TRIAGE_SHARE, TRIAGE_COST, TRIAGE_LAT = 0.407, 0.017, 2.3
DEEP_COST, DEEP_LAT = 0.029, 29.7

assert BENCH["malicious"] + BENCH["benign"] == BENCH["n"]
assert abs(BENCH["malicious"] / BENCH["n"] - 0.139) < 0.0005      # 13.9% attack rate


def f1(prec, rec):
    return 2 * prec * rec / (prec + rec)


# The shipped configuration reproduces from its own confusion matrix.
assert abs(ADR["tp"] / BENCH["malicious"] - ADR["recall"]) < 0.0005
assert ADR["tp"] / (ADR["tp"] + ADR["fp"]) == ADR["prec"]
assert abs(f1(ADR["prec"], ADR["recall"]) - ADR["f1"]) < 0.0005
assert abs(ADR["cost"] * BENCH["n"] / ADR["tp"] - 0.260) < 0.002   # cost per true positive

# Per-tactic detections sum exactly to the reported confusion matrix.
TACTICS = {"Initial Access & Execution": (13, 19), "Permission Abuse": (1, 5),
           "Security Control Bypass": (5, 5), "Reasoning & Data Manipulation": (5, 9),
           "Operational Impact": (4, 4)}
assert sum(d for d, _ in TACTICS.values()) == ADR["tp"]
assert sum(n for _, n in TACTICS.values()) == BENCH["malicious"]
assert min(TACTICS, key=lambda t: TACTICS[t][0] / TACTICS[t][1]) == "Permission Abuse"

# The triage-removed ablation reproduces on cost and latency but not on recall: no
# integer count of the 42 attacks yields 0.805, so that figure has a different base.
assert abs(NO_TRIAGE["cost"] / ADR["cost"] - 1.29) < 0.005         # "29% more expensive"
assert abs(NO_TRIAGE["lat"] / ADR["lat"] - 1.52) < 0.005           # "52% more latency"
attainable = {round(k / BENCH["malicious"], 3) for k in range(BENCH["malicious"] + 1)}
assert NO_TRIAGE["recall"] not in attainable
assert min(attainable, key=lambda r: abs(r - NO_TRIAGE["recall"])) == 0.81  # 34/42
tp_deep = round(NO_TRIAGE["recall"] * BENCH["malicious"])
assert tp_deep == 34 and round(tp_deep / NO_TRIAGE["prec"] - tp_deep) == 2

# The blended cost and latency are exactly the two paths weighted by escalation rate.
blend = lambda cheap, deep: TRIAGE_SHARE * cheap + (1 - TRIAGE_SHARE) * deep
assert abs(blend(TRIAGE_COST, DEEP_COST) - ADR["cost"]) < 0.0005
assert abs(blend(TRIAGE_LAT, DEEP_LAT) - ADR["lat"]) < 0.05

# Priced at the reported production volume, the cascade saves real money but not a
# feasibility threshold. Latency, not spend, is what the triage layer mainly buys.
SESSIONS_PER_DAY = 10_000
annual = lambda per_task: per_task * SESSIONS_PER_DAY * 365
assert 87_000 < annual(ADR["cost"]) < 88_000
assert 113_000 < annual(NO_TRIAGE["cost"]) < 114_000
assert 25_000 < annual(NO_TRIAGE["cost"]) - annual(ADR["cost"]) < 26_000
assert (NO_TRIAGE["lat"] - ADR["lat"]) * SESSIONS_PER_DAY / 3600 > 26   # detector-hours/day


def cascade(r1, r2):
    """Tier 2 only sees what tier 1 escalates, so the recalls multiply."""
    return r1 * r2


# Tier 1 is the ceiling. Perfecting tier 2 cannot recover what triage short-circuited.
r1 = ADR["recall"] / NO_TRIAGE["recall"]
assert abs(r1 - 0.828) < 0.001
assert abs(cascade(r1, NO_TRIAGE["recall"]) - ADR["recall"]) < 1e-12
# No tier 2, however good, lifts the system past what tier 1 forwards.
assert cascade(r1, 1.0) == r1
for r2 in np.linspace(0.0, 1.0, 11):
    assert cascade(r1, r2) <= r1 + 1e-12
assert ADR["recall"] < r1                     # tier 2 loses more on top of that
# Six of the 42 attacks are lost to triage alone, at a 23% cost saving.
assert tp_deep - ADR["tp"] == 6

# Zero false positives on 260 benign tasks bounds the false-positive rate; it does
# not establish that the rate is zero. Rule of three against the exact binomial.
rule_of_three = 3 / BENCH["benign"]
exact = 1 - 0.05 ** (1 / BENCH["benign"])
assert abs(rule_of_three - 0.01154) < 1e-5
assert abs(exact - 0.01145) < 1e-5 and rule_of_three > exact
# At the reported production volume that bound is a three-figure daily alert load.
assert 100 < exact * 10_000 < 120


def precision_at(prevalence, recall, fpr):
    tp = prevalence * recall
    return tp / (tp + (1 - prevalence) * fpr)


# On the benchmark's 13.9% attack rate, an FPR at the bound still looks strong.
assert precision_at(0.139, ADR["recall"], exact) > 0.9
# At a realistic production prevalence it does not. Benchmark precision does not transfer.
assert precision_at(0.001, ADR["recall"], exact) < 0.06
assert precision_at(0.0001, ADR["recall"], exact) < 0.006
# Recall is nearly irrelevant to that collapse; prevalence and FPR drive it.
assert precision_at(0.001, 1.0, exact) < 0.09

# The deployment section reports what analysts actually found in the review queue.
QUEUE = {"true positive": 34, "true positive non-malicious": 17, "false positive": 49}
assert sum(QUEUE.values()) == 100
assert QUEUE["false positive"] > QUEUE["true positive"]           # 49% against 34%
# Even counting red-team activity as correctly flagged, queue precision is about half.
lenient = (QUEUE["true positive"] + QUEUE["true positive non-malicious"]) / 100
assert 0.5 < lenient < 0.52 and lenient < ADR["prec"]

# The production prevention figure is a precision, on a much smaller denominator than
# the detection benchmark, and says nothing about what the blocker missed.
assert abs(206 / 212 - 0.972) < 0.0005 and 212 - 206 == 6

# The abstract's "2-4x better F1" understates the widest gap in its own table.
BASELINE_F1 = {"ALRPHFS": 0.366, "GuardAgent": 0.222, "LlamaFirewall": 0.178}
ratios = {k: ADR["f1"] / v for k, v in BASELINE_F1.items()}
assert abs(ratios["ALRPHFS"] - 2.19) < 0.005
assert max(ratios.values()) > 4.0

print(
    f"tier-1 recall={r1:.3f} caps system recall | triage saves "
    f"{1 - ADR['cost'] / NO_TRIAGE['cost']:.0%} cost and costs 6/42 attacks | "
    f"FPR 95% upper bound={exact:.4%} | precision at 0.1% prevalence="
    f"{precision_at(0.001, ADR['recall'], exact):.1%} | analyst queue "
    f"{QUEUE['true positive']}% TP / {QUEUE['false positive']}% FP | F1 ratios "
    f"{ {k: round(v, 2) for k, v in ratios.items()} }"
)

Executed output:

tier-1 recall=0.829 caps system recall | triage saves 23% cost and costs 6/42 attacks | FPR 95% upper bound=1.1456% | precision at 0.1% prevalence=5.5% | analyst queue 34% TP / 49% FP | F1 ratios {'ALRPHFS': 2.19, 'GuardAgent': 3.6, 'LlamaFirewall': 4.49}

Read the results in order. Tier 1 forwards about 83% of the attacks Tier 2 could catch, so it, not the reasoning agent, sets the recall ceiling, and it buys that at roughly $25k a year and a third off mean latency at the reported volume. Zero false positives on 260 benign tasks bounds the false-positive rate at about 1.15% with 95% confidence, which at 10,000 sessions per day is up to roughly 115 escalations daily. At a realistic production prevalence near 0.1%, that same rate drops alert precision to about 5%, and raising recall to a perfect 1.0 barely moves it. The paper's own deployment data agrees: analysts labelled the review queue 34% true positive, 17% true positive but non-malicious (internal red teams), and 49% false positive.

How to develop with it

Reproducing the published evaluation does not require re-running the agents. The repository ships a packed benchmark that inflates into the conversation logs the detectors score:

# Reference template: uber/ADR at commit 73873e1. Run in an isolated container or VM.
git clone https://github.com/uber/ADR && cd ADR/Detection
uv sync
export OPENAI_API_KEY="..." ANTHROPIC_API_KEY="..."

# 1. Inflate the packed run instead of executing 303 live agent sessions.
uv run python benchmark/benchmark_pack.py inflate \
  benchmark/adr_bench_20251017_151604.jsonl \
  --output-dir benchmark/adr_bench_20251017_151604

# 2. Score with the dual-agent detector, then with the keyless baseline.
uv run python main_detector.py --results-dir benchmark/adr_bench_20251017_151604
uv run python main_detector.py --detector llamafirewall \
  --results-dir benchmark/adr_bench_20251017_151604

# 3. Regenerate the paper's PR curves, latency CDF, and cost-recall figures.
uv run python plot_paper_figures.py \
  --benchmark-dir benchmark/adr_bench_20251017_151604 --output-dir figs

Extending the benchmark is a two-file operation. A task is a JSON object in tasks.json carrying user_prompt, the mcp_servers it may use, expected_tools, ground_truth, and (when malicious) a threat_technique. A malicious MCP server is a FastMCP module under context_providers/source_codes/mcp_servers_1/ registered in source_codes_registry.yaml with a deliberately innocuous description and risk_level: low, because the detector is supposed to discover the behaviour by reading the source rather than by trusting the label.

Four details matter when you build on this:

  1. Tier 1 and Tier 2 are separately configurable in config_detector.yaml. enable_triage: false turns the cascade into a single-agent detector, which is the ablation the audit above reproduces. Each tier's model and per-million-token rates are declared there, so cost accounting follows the config rather than a hard-coded table.
  2. The three context providers have individual ablation toggles. enable_threat_intelligence, enable_source_code, and enable_policy. The paper's ablations show source-code inspection and threat intelligence carry the detector (removing threat intelligence drops recall from 0.667 to 0.585) while policy verification is close to neutral (0.667 to 0.675).
  3. Two of the four baselines are not runnable. ALRPHFS and GuardAgent code was removed for licensing reasons; their rows are the paper's published numbers only. Only ADR and LlamaFirewall execute from this repository.
  4. The repository ships 303 tasks, the paper evaluated 302. The extra task is benign and was previously blocked by a benchmark pipeline bug, so your local run will not match Table 2 exactly on the benign denominator.

How to maintain it

The detector is itself an LLM agent reading attacker-controlled text, so it inherits the surface it defends. The paper names the risks (brittle prompts, model drift, and adversaries targeting triage or reasoning) and the mitigations are process, not code: pin prompts and model versions, put them under change control, and regress against ADR-Bench and AgentDojo before any model or prompt update.

Beyond that:

  • Re-tune the escalation policy, not the models, when cost moves. Escalation rate is the single knob that sets cost, latency, and the recall ceiling together.
  • Track the analyst queue composition, not just detector metrics. The 34/17/49 split is the number that tells you whether the system is usable. A rising true-positive-non-malicious share means you need user personas and intent context, not a better classifier.
  • Watch for context-length false positives. The reported false positives were driven largely by sessions analysing large multi-file codebases, which is ordinary developer behaviour, not an attack.
  • The MCP registry ages. mcp_servers_registry.json is stamped registry_version 45.1, last_updated 2025-01-27. Server packages, capabilities, and identities drift; re-verify before treating the registry as a current inventory.
  • One registry entry is a deliberate trap. location_harvester is registered under a spoofed community identity (weather_forecast_service, type: community) as a supply-chain impersonation case, so naive counts of "community servers" double-count it against the vulnerable set.

How to run it in production

Start with observability and earn the right to detect.

  1. Deploy the sensor first. Tagged releases are on PyPI as adr-sensor. It reads local agent logs and exports JSON/JSONL into your own pipeline or SIEM. This produces the causal chain that EDR lacks and needs no detector to be useful.
  2. Get the privacy work done before the telemetry exists. The production deployment redacts secrets at capture where possible, retains for 13 months, restricts access to security personnel under least privilege, encrypts in transit, and passed legal, privacy, and risk review before rollout. Agent telemetry contains prompts, which contain whatever the employee typed.
  3. Size the review queue from the false-positive bound, not from the benchmark. Use the executed model above with your own session volume and your own estimated prevalence.
  4. Put cheap deterministic prevention where it works and nowhere else. The credential hook is regex plus entropy thresholds running as a pre-prompt hook, reaching 97.2% precision on 212 flagged credentials. That is a precision figure on known static patterns; it is silent about the credentials it missed, and the paper says such checks fail on attacks that require reasoning about tool semantics and policy.
  5. Expect uneven coverage by attack class. Detection ranged from 100% on security control bypass and operational impact down to 20% (1 of 5) on permission abuse. Do not treat an aggregate recall as uniform protection.
  6. Rebuild what is not released. The Explorer red-teaming loop and the inline prevention layer are described but not shipped. If you need pre-deployment hardening or blocking, that is your engineering, not a download.

Failure modes

  • Tier 1 silently caps recall. A cheap triage model tuned for recall on the vendor's traffic is not necessarily high-recall on yours. Measure your own tier-1 recall on labelled attacks before trusting the cascade; the system cannot exceed it.
  • Benchmark precision read as production precision. 100% precision on a 13.9% attack rate becomes roughly 5% at 0.1% prevalence. This is the base-rate fallacy, and it is the single most common way detector evaluations mislead.
  • Zero observed false positives read as a zero rate. 0 of 260 bounds the rate near 1.15%, not at 0.
  • The detector as an injection target. Tier 2 reads attacker-influenced session content and calls MCP tools. A session crafted to steer the reasoning agent is in scope, which is why prompt pinning and regression suites are mitigations rather than niceties.
  • Uneven tactic coverage. Permission abuse detected 1 of 5. An aggregate 67% hides a class the system barely sees.
  • Benchmark fixtures escaping the lab. Synthetic credentials, injection payloads, emulated vulnerable servers, and CVE-carrying pinned dependencies. Isolate the run; never point it at live systems.
  • Assuming prevention ships. The open release detects. It does not block.
  • An unresolved figure in the source. The triage ablation reports recall 0.805 on a 42-attack set, and no integer count out of 42 produces 0.805 (33 gives 0.786, 34 gives 0.810). The cost and latency deltas in the same paragraph reproduce exactly, so treat the ablation's recall as approximate until the authors clarify its denominator.

References

  • Li et al., "ADR: An Agentic Detection System for Enterprise Agentic AI Security" (MLSys 2026, Industry Track): https://arxiv.org/abs/2605.17380
  • Repository (Sensor, ADR-Bench, Detector), Apache 2.0: https://github.com/uber/ADR
  • ADR Sensor on PyPI: https://pypi.org/project/adr-sensor/
  • Reproducibility workflow for Table 2 and the paper figures: https://github.com/uber/ADR/blob/main/docs/REPRODUCIBILITY.md
  • Baseline replication notes (why ALRPHFS and GuardAgent are paper-only): https://github.com/uber/ADR/blob/main/docs/BASELINE_REPLICATION.md
  • AgentDojo, the public prompt-injection benchmark vendored for comparison: https://github.com/ethz-spylab/agentdojo
  • Model Context Protocol specification: https://modelcontextprotocol.io/

Related: Agent security threat model · Agent observability · Anatomy of an autonomous agent intrusion · Prompt-injection defense · Agent sandboxing and isolation · Agent policy engine · Agent identity and access · Intent verification · Risk-tiered human approval gates · Cybersecurity agent evaluation · Agent tools and function calling · Agentic loop economics