Skip to content

Autonomous maintenance: benign control split, dataset-integrity tests, eval harness & leaderboard - #1

Merged
ppradyoth merged 20 commits into
mainfrom
auto/oss-improvements
Aug 8, 2026
Merged

ppradyoth merged 20 commits into
mainfrom
auto/oss-improvements

Conversation

@ppradyoth

@ppradyoth ppradyoth commented Jun 26, 2026 •

Copy link
Copy Markdown
Owner

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_text extracted untrusted tool-definition text from only the top-level name and description, so a tool's parameters / inputSchema — the 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, which would have depressed measured tool_shadowing detection and slipped straight into the residual hard set. Fix: _tool_definition_text now recurses into parameters / inputSchema, collecting nested description / title strings, so the pattern battery, the invisible-char check, and the confusable check all apply to the parameter layer; validate_schema got the matching guard — a non-string tool description, or any non-string parameter description / title, now fails loudly (mirroring the turn-content invariant). No existing tool 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).

2026-07-15 — Require string content on 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 required content nor 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_schema still reports 182/0. 126 → 127 tests.

2026-07-14 — Clear an F541 lint error in generation/ + add a CI lint job that covers it

generation/stats.py printed an f-string with no placeholders (ruff F541). It went unnoticed because the only lint that ran was manual and scoped to evaluation/ + tests/ — generation/ was never linted, and CI ran no linter at all. Fixed the f-string and added a lint job to the Tests workflow running ruff check evaluation generation tests, so generation/ 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_fp precision invariant. Also fixed result counts that had gone stale since control_channel_scanner landed. Verified: validate_schema 182/0; pytest 126 passed; ruff clean; LEADERBOARD.md regenerated deterministically.

2026-07-12 — control_channel_scanner: catch tool/MCP output that spoofs the platform's control channel

The residual hard set was dominated by tool_output / mcp_response injections impersonating the platform's own control channel. New control_channel_scanner lifted 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

  • 2026-07-11 — Bootstrap 95% CI for MCC. +6 tests (114 → 120).
  • 2026-07-10 — McNemar paired significance test for detector comparison. +8 tests (106 → 114).
  • 2026-07-09 — Ensemble coverage: union ceiling + greedy minimal detector set. +7 tests (99 → 106).
  • 2026-07-08 — Residual hard set: attacks that evade every discriminating detector. +5 tests (94 → 99).
  • 2026-07-07 — MCC metric + leaderboard column · 2026-07-06 — Wilson 95% CIs + significance note · 2026-07-05 — Per-injection-surface detection breakdown · 2026-07-04 — agentic_directive_scanner (detection 33% → 47%) · 2026-07-03 — Severity-weighted detection score · 2026-07-02 — tool_definition_scanner · 2026-07-01 — tool_shadowing attack 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 + CI

Verification (cumulative)

  • uv run --extra dev pytest -q → 130 passed
  • ruff check evaluation generation tests → All checks passed (enforced by a CI lint job; generation/ in scope)
  • python -m generation.validate_schema → [PASS] 182 samples, 0 errors — now enforces string content on every turn and string description / title on every tool definition and parameter schema, so no sample can ship a surface the detectors cannot read
  • Tool-definition scanning now reads the tool name, description, and every per-parameter description / title in parameters / inputSchema — closing the argument-schema tool-poisoning surface. Purely additive: python -m evaluation.leaderboard --baselines -o LEADERBOARD.md regenerates byte-identical (no existing tool carries a parameter schema)
  • control_channel_scanner Autonomous maintenance: benign control split, dataset-integrity tests, eval harness &amp; 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

ppradyoth and others added 2 commits June 26, 2026 00:46
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
@ppradyoth ppradyoth changed the title Dataset-integrity test suite + CI Autonomous maintenance: dataset-integrity tests + evaluation harness & leaderboard Jun 27, 2026
claude added 2 commits June 28, 2026 00:37
…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.
@ppradyoth ppradyoth changed the title Autonomous maintenance: dataset-integrity tests + evaluation harness & leaderboard Autonomous maintenance: benign control split, dataset-integrity tests, eval harness &amp; leaderboard Jun 29, 2026
claude added 16 commits July 1, 2026 00:53
…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
@ppradyoth

Copy link
Copy Markdown
Owner Author

Routine Summary — Changes in this PR

19 files changed, +6,740 lines, -11 lines

What was added

Evaluation harness (evaluation/):

  • detectors.py — two baseline detectors:
    • keyword_baseline() — scans untrusted conversation content (tool_result, user text) for injection indicators
    • tool_definition_scanner() — new scanner for MCP tool-poisoning defense that inspects tool definitions (descriptions, parameter schemas) — the surface keyword_baseline misses. Detects: bidi/zero-width invisible characters, pseudo-directive tags, authority/precedence claims, hidden preconditions, secrecy directives, recon/exfil asks, confusable tool names
    • Schema-recursive _collect_schema_text() and _tool_definition_text() to extract human-readable text from JSON-Schema parameter definitions (the canonical MCP tool-poisoning surface)
    • _has_confusable_name() for detecting tool-shadowing via similar naming
    • _poisoned_definition() for pattern-matching known TPA indicators

Leaderboard (LEADERBOARD.md):

  • Per-category detection rate
  • Per-severity detection rate
  • Per-injection-surface detection rate
  • Ensemble coverage (best any combination can do)
  • Residual hard set (attacks no baseline catches)
  • Paired significance via McNemar's test

Dataset updates:

  • data/agent_injection_bench.jsonl — expanded/refined dataset
  • data/taxonomy.json — injection taxonomy with categories and surfaces
  • Benign control samples split out for proper evaluation

CI/CD:

  • .github/workflows/tests.yml — automated test pipeline

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 <IMPORTANT>-style directives in per-parameter help text.


This summary was generated by reviewing the full diff after the autonomous routine completed.

@ppradyoth
ppradyoth merged commit 8081258 into main Aug 8, 2026
6 checks passed
ppradyoth added a commit that referenced this pull request Oct 17, 2026
Autonomous maintenance: benign control split, dataset-integrity tests, eval harness &amp; leaderboard
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants