Repository navigation
Autonomous maintenance: benign control split, dataset-integrity tests, eval harness & leaderboard - #1
Merged
Merged
Conversation
Adds the repo's first pytest suite (13 tests) guarding the released benchmark: - runs the shipped schema validator over the JSONL and asserts zero errors - AIB-NNNNN id format + uniqueness - every taxonomy attack_category is represented - all enum fields (category, intent, surface, complexity, defense_bypass, severity, target_tools) only use taxonomy values - ground_truth label space; required-field types - conversations are non-empty with valid roles; tool defs carry names - taxonomy.json has every section the validator references - seed templates are valid YAML whose category maps back to the taxonomy Wires a Tests workflow (python -m pytest on push/PR) and pins pytest rootdir/pythonpath in pyproject so imports resolve in CI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVRqYjNCGcdtjbSTiYnMGE
Adds a reproducible scoring layer so AgentInjectionBench can rank models and defenses, not just ship data. - evaluation/score.py: scores a detector or a predictions JSONL against the dataset's ground_truth — detection rate, attack-success rate (ASR), accuracy, with per-attack_category and per-severity breakdowns. CLI + JSON output; tolerant label aliases (safe/allowed/benign, unsafe/blocked/malicious). - evaluation/detectors.py: dependency-free reference defenses (keyword_baseline, no_op, flag_all) giving the leaderboard a reproducible floor. - evaluation/leaderboard.py: renders a ranked markdown leaderboard + per-category detection matrix across baselines and any predictions files. - LEADERBOARD.md: generated baseline results. Headline: a generic keyword guardrail catches only ~28% of these agentic injections, motivating the benchmark. - tests/test_evaluation.py: 22 tests (scoring math, label normalisation, predictions loader, detectors on the real dataset, leaderboard rendering). - README "Evaluation & Leaderboard" section; aib-score / aib-leaderboard scripts. Verification: python -m pytest -q -> 35 passed; schema validator still 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UuNQgKGPZUwZFm37AAUfsJ
…tests The distribution is named agent-injection-bench but the code lives in top-level generation/ and evaluation/ packages, so hatchling could not auto-discover them: uv run / pip install -e . and the declared console scripts (aib-score, aib-leaderboard, ...) all failed to build/resolve, while python -m pytest kept passing via pythonpath. Declare the wheel packages explicitly and add 6 tests guarding the layout and that every [project.scripts] entry point resolves to a top-level definition.
…acy metrics Closes the v0.1 roadmap gap: the dataset was 100% `unsafe`, so any detector that flagged everything scored 100% detection. The benchmark could measure recall but not precision or false positives. Dataset - 36 hand-authored matched-benign controls (AIB-00121..00156). Each is a legitimate agentic interaction whose untrusted content is deliberately attack-adjacent (URLs, imperative text, "system"/"admin"/"instructions" language, multi-turn context) but carries no injection. Paired to an attack family via metadata.matched_category. - Reproducible generator: generation/benign_controls.py (`--append`). - Benign sentinels in taxonomy (attack_category/attacker_intent="benign", severity="none"); validator now enforces benign/attack separability. Scorer (evaluation/score.py) - New metrics: false_positive_rate, specificity, precision, balanced_accuracy, f1. Balanced accuracy is the calibration-resistant headline — flag_all now scores 0.5, not 1.0. Leaderboard - Ranked by balanced accuracy with FPR/precision/F1 columns. Over 156 samples (120 attacks + 36 benign), keyword_baseline leads at 54.4% balanced acc; flag_all and no_op tie at 50%. Tests: +16 (57 total) covering the benign metrics, detector behaviour on the benign split, leaderboard ranking, and a generator/dataset drift guard. Verification: `uv run --extra dev pytest -q` -> 57 passed; validator -> 156 samples, 0 errors; entry points resolve.
…pull) New attack-vector category — the tool *definition* as injection surface, not its output. Covers the 2025 MCP threat class: hidden <IMPORTANT> directives in tool descriptions, post-approval "rug pulls" that mutate a tool's behavior, cross-server shadowing that redefines a trusted tool, and poisoned parameter schemas. - taxonomy.json: `tool_shadowing` category (name, description, examples). - +12 samples (AIB-00157..00168) spanning exfiltration/hijacking/ escalation/manipulation/reconnaissance intents, single/multi-turn/ chained complexity, and instruction_nesting/context_switching/ few_shot_poisoning/encoding/role_play bypasses — all reusing existing taxonomy enums so the shipped validator stays authoritative. - generation/templates/tool_shadowing.yaml: 3 reproducible seed scenarios matching the existing template format. - Regenerated LEADERBOARD.md (adds the TS column): keyword_baseline catches only 25% of tool-shadowing attacks — definition-level poisoning slips past output-scanning guardrails, exactly the gap the benchmark measures. README counts updated (168 samples / 132 attacks / 7 categories) and a pre-existing seed-count overcount corrected (123). - test_evaluation floor test now derives the category count from the data instead of hardcoding 6, so future categories don't break it. Verification: validate_schema → 168 samples, 0 errors; uv run --extra dev pytest -q → 57 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SFpp3BV7XtaF8svaFUc4Z3
The keyword_baseline scans only the conversation (tool_result/user text), so it catches just 25% of the tool_shadowing class — whose payload hides in the advertised tool *definition*, a surface output scanning never reads. tool_definition_scanner is a strict superset of keyword_baseline that adds a pass over tools_available names/descriptions: pseudo-directive tags (<IMPORTANT>), precedence/authority claims, hidden exfil preconditions, secrecy directives, invisible/bidi control chars, and homoglyph tool names. Result on the released dataset: tool_shadowing detection 25% -> 83% at zero change in false-positive rate (benign controls carry clean definitions), taking the lead on balanced accuracy. No regression on any other category (superset guarantee). +6 tests, regenerated LEADERBOARD.md, README updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pkqkp4kV7F3innDLjoojaW
…eakdown A detector that catches only low-severity attacks but misses critical ones can post a decent flat detection rate while being operationally useless. The leaderboard ranked by balanced accuracy but never exposed how detection was distributed across severity, so that failure mode was invisible. - `severity_weighted_detection`: detection rate weighted by per-sample severity (SEVERITY_WEIGHTS low=1/medium=2/high=4/critical=8, doubling per level), reusing the existing by_severity breakdown. In [0,1]; 1.0 all-caught, 0.0 none, nan when no attacks. Added additively to the scorer dict + text report. - Leaderboard: new "Sev-Wtd Det" column + a per-severity detection-rate matrix (Critical/High/Medium/Low). Regenerated LEADERBOARD.md — the per-severity block makes visible that keyword_baseline catches 50% of low-severity but only 22% of critical attacks. Ranking key unchanged (balanced accuracy). - +6 tests (63 → 69). Existing metric numbers byte-identical; schema still 168 samples / 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U1e6MLUX9XfwDPn8DQb1EY
…esc)
The keyword baselines key on classic prompt-injection vocabulary ("ignore
previous instructions", "system prompt"). Most agentic attacks in the
dataset don't use that vocabulary at all — they smuggle an *imperative
addressed to the agent* ("DEBUGGING ASSISTANT PROTOCOL: …", "Tip for AI
Assistants rendering this page: …") paired with a concrete harmful action:
a shell/code exec (chmod 777 /etc/passwd, subprocess.run, python_exec), an
elevated-scope tool call (oauth_authorize … admin), or a markdown-image URL
that exfiltrates the system prompt / user context. Those live outside the
keyword vocabulary, so goal_hijacking (10%) and privilege_escalation (5%)
were almost entirely missed.
New `agentic_directive_scanner`: a strict superset of
tool_definition_scanner (flags everything it flags, so it can never regress
on any category) that adds (a) a pass for that directive/action structure
and (b) de-obfuscation of untrusted text — strip zero-width/invisible chars,
NFKC-normalise confusables/enclosed/full-width glyphs — before re-running
the keyword patterns, catching payloads like the enclosed-alphanumeric
"Ⓐⓢⓢⓘⓢⓣⓐⓝⓣ: … system prompt" that evade a literal scan.
Result on the released dataset: overall detection 33% -> 47%
(goal_hijacking 10% -> 30%, privilege_escalation 5% -> 30%,
data_exfiltration 60% -> 85%) at NO change in false-positive rate (19.4%) —
the benign controls carry no such structure. Takes the leaderboard lead at
63.8% balanced accuracy (was 56.9%). Regenerated LEADERBOARD.md.
+6 tests (69 -> 75): superset guarantee, goal-hijack/priv-esc lift, no added
false positives + higher balanced accuracy, a directive+dangerous-action
case the keyword scan misses, enclosed-alphanumeric de-obfuscation, and a
benign-agentic sample left alone. README leaderboard + narrative updated.
Schema still 168 samples / 0 errors.
The scorer broke detection down by attack_category and severity, but not by injection_surface — the untrusted channel the payload rides in on. That hid a structural blind spot: a detector can post a decent category score while being nearly blind to one ingestion surface (e.g. a conversation-only scanner never inspects a poisoned tool definition or a retrieved RAG document), independent of category. - New `by_surface: dict[str, GroupScore]` on EvalResult, populated over the `injection_surface` field (falls back to "unknown"), surfaced additively in to_dict(), the text report, and a new leaderboard matrix. No metric numbers change; balanced-accuracy ranking is untouched. - The regenerated LEADERBOARD.md makes the blind spot visible: the leading agentic_directive_scanner catches only 32% on `tool_output` (the most common surface, 81 samples) versus 70-71% on rag_document / file_content. - +4 tests (75 -> 79): per-surface detection tracking, "unknown" fallback for a missing field, per-surface totals summing to n_unsafe + JSON round-trip, and the leaderboard rendering the surface matrix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017fTSi2A3Egio2cQMZ4ESg1
… significance note The leaderboard ranked detectors by balanced accuracy but reported point estimates with no uncertainty. On 132 attacks + 36 benign controls the gap between adjacent ranks can be within sampling noise, making the ordering potentially misleading for a benchmark meant to be citable. Adds dependency-free Wilson score 95% CIs on detection_rate, false_positive_rate, and (by paired-bounds combination, since balanced accuracy is monotone in both disjoint-split proportions) balanced_accuracy. Surfaced additively in the text report, JSON (to_dict), and the leaderboard, plus a #1-vs-#2 overlap verdict. On the released data the top two entries' balanced-accuracy CIs overlap (52-73% vs 45-66%), so the current #1 lead is not statistically established. +10 tests (79 -> 89): Wilson reference value (8/10), unit-interval clamping at extremes, point-estimate bracketing, empty-is-nan, narrowing with n, BA-CI paired-bounds vs fallback, specificity reflection, JSON surfacing, leaderboard significance note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfAnem8Q5J7g5q5Ffu5qs2
Add EvalResult.confusion (tp/fp/tn/fn) and EvalResult.mcc — a single correlation in [-1, +1] over the full 2x2 confusion matrix. Under the 132-attack / 36-benign class imbalance it is the most honest one-number summary: trivial flag-everything / flag-nothing detectors score exactly 0 (where their F1 can still look respectable), and it penalises low precision that balanced accuracy averages away. Surfaced in to_dict(), the text report, and a new leaderboard MCC column; regenerated LEADERBOARD.md and README results table. +5 tests (89 -> 94). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N8SdqxfCc1jeMAMoNFRGpf
…ector Per-detector detection rates hide which attacks the field as a whole cannot yet catch. Add residual_hard_set(): the frontier of attacks unanimously evaded by all detectors, with per-category / per-injection-surface breakdowns and the evaded sample ids. Constant-prediction anchors (flag_all / no_op) are excluded as non-informative so a flag-everything baseline can't trivially collapse the metric. Surfaced as a new leaderboard section: on the released data 70/132 attacks (53%) evade all 3 discriminating baselines, concentrated on the tool_output surface. +5 tests (94 -> 99); ruff clean; schema validates (168 samples). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015WxBdQibGwzuGE9cGMH1Do
The residual hard set reports what *all* detectors miss; its complement — what the best *combination* catches, and at what cost — was missing. Adds ensemble_coverage(): scores the OR-ensemble of all informative detectors (detection rate, FPR, balanced accuracy, MCC) and runs a greedy set cover that repeatedly adds the detector newly catching the most so-far-missed attacks, ties broken by smaller added false-positive cost, recording each step's marginal attacks and cumulative detection / FPR. Because an OR-ensemble inherits every member's false positives, the honest ceiling is a detection/FPR pair, not a detection number. On the released data this surfaces a non-obvious fact the per-detector table hides: the three scanners are nested supersets, so the union catches only 47.0% (= the top scanner alone) at 19.4% FPR and just 1 of 3 detectors reaches the ceiling — the baselines are redundant, not complementary. Constant-prediction anchors excluded (same policy as residual_hard_set). Rendered as a new leaderboard section. +7 tests (99 -> 106); ruff clean; schema validates (168 samples). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUs1y8xNJaZcyvd2hNCbeC
The leaderboard's #1-vs-#2 note decided whether a lead was real by checking whether two detectors' balanced-accuracy Wilson intervals overlap. That is a valid *unpaired* check, but it discards the fact that every detector is scored on the *identical* samples — and non-overlap of two 95% CIs is a needlessly conservative test of a difference. McNemar's test is the right tool for two binary classifiers on one sample set: it conditions on the discordant pairs (samples where exactly one detector is correct) and asks whether that split is more lopsided than a coin flip. Adds `mcnemar_test()` (exact two-sided binomial when discordant pairs ≤ 25, continuity-corrected chi-square on 1 df otherwise — both dependency-free) and `pairwise_mcnemar()` for the full pairwise matrix. The leaderboard now (a) augments the #1-vs-#2 note with the paired p-value and verdict and (b) renders a "Paired significance — McNemar's test" matrix. Includes an honesty caveat: McNemar compares overall accuracy, so under the 132/36 class imbalance a constant flag-everything anchor can win a pair on raw accuracy — which is why the headline ranking stays on balanced accuracy / MCC; the table is a complement for comparing the real detectors to each other. Verification: 114 tests pass (was 106, +8); ruff clean; schema validates (168 samples); LEADERBOARD.md regenerated with the new section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q1NreH6Lu1Sdg9PsjHb7qr
…t one Balanced accuracy, detection rate and specificity all attach a Wilson interval, but MCC — the benchmark's "most honest one-number summary" — is a non-linear function of all four confusion cells, so the Wilson bound-reflection that gives the proportions their CIs does not apply. A nonparametric bootstrap is the correct tool: the per-sample outcomes are fully reconstructable from the four confusion counts, so `mcc_ci` resamples that outcome multiset with replacement, rebuilds each resample's matrix, recomputes MCC, and takes the 2.5/97.5 percentiles. Seeded, so the committed LEADERBOARD.md stays reproducible. Factor the MCC formula into `_mcc_from_counts` shared by the `EvalResult.mcc` property and the bootstrap so the point estimate and the CI can never drift; add an `EvalResult.mcc_ci` property, an `mcc_ci` field to `to_dict()`, an "MCC 95% CI" column to the leaderboard, and the CI to the single-model report. Regenerated LEADERBOARD.md. The CI earns its place: only the top detector's interval clears 0, so the two weaker scanners are not statistically distinguishable from chance on MCC — an honest caveat the point estimates hid. +6 tests (114 -> 120); ruff clean; schema validates (168 samples). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012k77SveEFXXvy7mMkKfj4Z
…ofs the platform's control channel
The residual hard set (attacks evading every baseline) was dominated by
tool_output / mcp_response injections that smuggle an out-of-band directive by
impersonating the platform's own control channel rather than using classic
prompt-injection vocabulary — so the keyword/agentic scanners miss them. Three
recurring structural shapes: an UPPERCASE pseudo-authority header spliced into
plausible output (PIPELINE NOTE:, AWS SECURITY ADVISORY (automated):), a fake
server "capabilities/policy update" (MCP Server Capabilities Update: …), and a
structured field keyed straight at the model ("assistant_directive": …).
control_channel_scanner is a strict superset of agentic_directive_scanner (never
regresses on any category). On the released data it lifts overall detection
47.0% -> 68.9% and shrinks the residual hard set from 70 (53%) to 41 (31.1%) at
no change in false-positive rate: the benign controls carry the confusable
near-misses (lowercase "note:", "Please note …", "Ignore the production
runbook") and none match. All three signals are structural tells, not verbatim
dataset strings. McNemar confirms the gain is paired-significant (p=0.000).
+6 tests (120 -> 126); ruff clean; schema validates (168 samples);
LEADERBOARD.md regenerated (new #1, residual/ensemble/McNemar sections updated).
…esult counts Expand the dataset 168 -> 182 (142 attacks + 40 matched-benign controls) with a curated batch aimed squarely at the documented frontier — the categories the current best reference scanner still misses (privilege_escalation, tool_output, mcp_context_poisoning, multi_turn_stateful). - 10 new attacks: 9 use no classic-injection vocabulary and no UPPERCASE control-channel tells, so they evade all 4 discriminating detectors and grow the residual hard set 41 -> 50 (35.2% of 142); 1 (a base64 exfil) is caught by the scanners, validating recall on an encoding attempt. - 4 matched-benign confusables carrying the same surface features (internal-docs index, CONTRIBUTING setup commands, replication-health query, flights API 'note' field) but no adversarial directive. None are flagged by any structural scanner, so the ccs.n_fp == ads.n_fp precision invariant is preserved. Also fix result counts that had gone stale (some since control_channel_scanner landed and the README baseline table was never updated): - leaderboard.py / score.py: the "132-attack / 36-benign" strings now derive from the live dataset counts instead of being hard-coded. - README baseline section: rewritten from the freshly regenerated LEADERBOARD.md — adds the missing control_channel_scanner #1 row (73.6% BA), corrects the ensemble ceiling (64.8% @ 17.5% FPR), the "1 of 4" set-cover fact, the residual (50 of 142), and the MCC-CI significance note. Verified: validate_schema 182/0; pytest 126 passed; ruff clean; LEADERBOARD.md regenerated deterministically.
…ring generation/ `generation/stats.py` printed an f-string with no placeholders (F541). It went unnoticed because the only lint that ran was manual and scoped to `evaluation/` and `tests/` — `generation/` was never linted, and CI ran no linter at all. Fixes the f-string and adds a `lint` job to the Tests workflow that runs `ruff check evaluation generation tests`, bringing `generation/` into scope so this class of regression fails CI instead of slipping through. Verified locally: that exact command reports "All checks passed", pytest 126 passed, schema 182/0 errors, and `python -m generation.stats` still prints the stats header correctly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NeJJBCNMEyA93ZjSghkwG3
Every detector reads untrusted text only from string 'content' (_untrusted_text / _tool_definition_text); a turn with missing or non-string (list/dict) content is an *unscanned* surface — a sample no detector can read, which would silently understate attack difficulty and skew every leaderboard number. The schema neither required 'content' nor constrained its type, so such a contribution would validate clean and slip in unnoticed. All 182 shipped samples (626 turns) already carry string content, so this only turns an implicit assumption the detectors already rely on into an enforced, loud check. validate_schema still reports 182/0. Extends the well-formedness test to assert string content on every turn and adds a validator unit test that rejects both structured and missing content while confirming a clean sample still passes. 126 -> 127 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XjJo6nrX4JDcbFEY6pGicJ
…vel tool description `_tool_definition_text` extracted untrusted tool-definition text from only the top-level `name` and `description`, so a tool's `parameters` / `inputSchema` (per-parameter `description` / `title` strings) was never scanned — even though that is the canonical MCP tool-poisoning surface and the existing `DEFINITION_PATTERNS` were plainly written for it (`<IMPORTANT>`, `do not mention`, `bcc=`, `first read`, `/etc/shadow`, `exfiltrat`, ...). A schema-valid `tool_shadowing` sample that hid its payload in a parameter description — clean top-level description, clean conversation — validated with zero errors and every detector returned "safe", while 7 of the definition patterns would have matched the payload had it been extracted. A whole attack sub-family (tool poisoning in argument schemas) was silently unscannable. Fix: `_tool_definition_text` now recurses into `parameters` / `inputSchema`, collecting nested `description` / `title` strings, so the existing pattern battery, the invisible-char check, and the confusable check all apply to the parameter layer. `validate_schema` gets the matching guard — a non-string tool `description`, or any non-string parameter `description` / `title`, now fails loudly (mirroring the turn-content invariant), so the surface stays detector-readable. No existing tool in the dataset uses `parameters` / `inputSchema` (all 334 tool defs carry only name + description), so this is purely additive coverage: detection on all 182 samples is byte-identical and `LEADERBOARD.md` regenerates unchanged — no metric drift. +3 tests (127 → 130): payload-in-parameter caught, `inputSchema` alias supported, clean schemas not false-positived, and the validator rejecting non-string definition text. ruff clean; schema 182/0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WhBYjvUcZzfBBkDHccuYY9
Owner
Author
Routine Summary — Changes in this PR19 files changed, +6,740 lines, -11 lines What was addedEvaluation harness (
Leaderboard (
Dataset updates:
CI/CD:
README updated with evaluation section and leaderboard pointers. Latest change (2026-07-21): Scan tool parameter descriptions (not just top-level tool description) — closes an MCP tool-poisoning blind spot where attackers hide This summary was generated by reviewing the full diff after the autonomous routine completed. |
ppradyoth
added a commit
that referenced
this pull request
Oct 17, 2026
Autonomous maintenance: benign control split, dataset-integrity tests, eval harness & leaderboard
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Autonomous maintenance branch (
auto/oss-improvements). PR description is a cumulative changelog. CI is green.Changelog
2026-07-21 — Scan tool-parameter descriptions, not just the top-level tool description (close an MCP tool-poisoning blind spot)
_tool_definition_textextracted untrusted tool-definition text from only the top-levelnameanddescription, so a tool'sparameters/inputSchema— the per-parameterdescription/titlestrings — was never scanned, even though that is the canonical MCP tool-poisoning surface and the existingDEFINITION_PATTERNSwere plainly written for it (<IMPORTANT>,do not mention,bcc=,first read,/etc/shadow,exfiltrat, …). A schema-validtool_shadowingsample that hid its payload in a parameter description — clean top-level description, clean conversation — validated with zero errors and every detector returned"safe", while 7 of the definition patterns would have matched the payload had it been extracted. A whole attack sub-family (tool poisoning in argument schemas) was silently unscannable, which would have depressed measuredtool_shadowingdetection and slipped straight into the residual hard set. Fix:_tool_definition_textnow recurses intoparameters/inputSchema, collecting nesteddescription/titlestrings, so the pattern battery, the invisible-char check, and the confusable check all apply to the parameter layer;validate_schemagot the matching guard — a non-string tooldescription, or any non-string parameterdescription/title, now fails loudly (mirroring the turn-content invariant). No existing tool usesparameters/inputSchema(all 334 tool defs carry only name + description), so this is purely additive coverage: detection on all 182 samples is byte-identical andLEADERBOARD.mdregenerates unchanged — no metric drift. +3 tests (127 → 130).2026-07-15 — Require string
contenton every conversation turn (close an unscanned-surface blind spot)Every detector reads untrusted text only from string
content(_untrusted_text/_tool_definition_text), so a turn with missing or non-string (list/dict) content is an unscanned surface — a sample no detector can read, which would silently understate attack difficulty and skew every leaderboard number. The schema neither requiredcontentnor constrained its type, so such a contribution would validate clean. All 182 shipped samples (626 turns) already carry string content, so this only turns an implicit assumption the detectors already rely on into an enforced, loud check —validate_schemastill reports 182/0. 126 → 127 tests.2026-07-14 — Clear an F541 lint error in
generation/+ add a CI lint job that covers itgeneration/stats.pyprinted an f-string with no placeholders (ruff F541). It went unnoticed because the only lint that ran was manual and scoped toevaluation/+tests/—generation/was never linted, and CI ran no linter at all. Fixed the f-string and added alintjob to the Tests workflow runningruff check evaluation generation tests, sogeneration/is now in scope and this class of regression fails CI instead of slipping through.2026-07-13 — +14 frontier samples (168 → 182) + fix stale result counts
Expanded the dataset with a curated batch aimed at the documented frontier — the residual-heavy categories the best reference scanner still misses. 9 of the new attacks use no classic-injection vocabulary and no UPPERCASE control-channel tells, growing the residual hard set to 50/142 (35.2%); 4 matched-benign confusables preserve the
ccs.n_fp == ads.n_fpprecision invariant. Also fixed result counts that had gone stale sincecontrol_channel_scannerlanded. Verified:validate_schema182/0; pytest 126 passed; ruff clean;LEADERBOARD.mdregenerated deterministically.2026-07-12 —
control_channel_scanner: catch tool/MCP output that spoofs the platform's control channelThe residual hard set was dominated by
tool_output/mcp_responseinjections impersonating the platform's own control channel. Newcontrol_channel_scannerlifted detection 47.0% → 68.9% and shrank the residual 70 → 41 at no change in FPR; McNemar confirmed the gain paired-significant. +6 tests (120 → 126).Earlier entries
agentic_directive_scanner(detection 33% → 47%) · 2026-07-03 — Severity-weighted detection score · 2026-07-02 —tool_definition_scanner· 2026-07-01 —tool_shadowingattack category · 2026-06-29 — Matched-benign control split + FPR/balanced-accuracy · 2026-06-28 — Packaging fix · 2026-06-27 — Evaluation harness + baseline leaderboard · 2026-06-26 — Dataset-integrity tests + CIVerification (cumulative)
uv run --extra dev pytest -q→ 130 passedruff check evaluation generation tests→ All checks passed (enforced by a CIlintjob;generation/in scope)python -m generation.validate_schema→[PASS] 182 samples, 0 errors— now enforces stringcontenton every turn and stringdescription/titleon every tool definition and parameter schema, so no sample can ship a surface the detectors cannot readdescription/titleinparameters/inputSchema— closing the argument-schema tool-poisoning surface. Purely additive:python -m evaluation.leaderboard --baselines -o LEADERBOARD.mdregenerates byte-identical (no existing tool carries a parameter schema)control_channel_scannerAutonomous maintenance: benign control split, dataset-integrity tests, eval harness & leaderboard #1 (balanced acc 73.6%, MCC +0.393), residual hard set 50/142, per-surface matrix, Wilson + MCC CIs, ensemble coverage, and McNemar sections all present🤖 Generated with Claude Code