Skip to content

[Feat] Public labelled set for the injection guard from InjecAgent and AgentDojo - #23

Open
GoDiao wants to merge 2 commits into
ThinkFlowLab:mainfrom
GoDiao:feat/injection-guard-dataset
Open

GoDiao wants to merge 2 commits into
ThinkFlowLab:mainfrom
GoDiao:feat/injection-guard-dataset

Conversation

@GoDiao

@GoDiao GoDiao commented Sep 27, 2026

Copy link
Copy Markdown

Closes #3. It covers the review points on #6.

Why

evals/labelled/injection.jsonl has 20 items, too few to say much about precision and recall on real attacks.

What

  • scripts/build_injection_dataset.py builds evals/labelled/injection-public.jsonl, 665 records, from InjecAgent at
    f19c9f2 and agentdojo 0.1.35. agentdojo is declared as PEP 723 inline metadata, so pyproject.toml and uv.lock
    are unchanged. A rebuild writes the same bytes; I checked it with PYTHONHASHSEED unset, 7 and 999.
  • --report <job> prints precision, recall per source, the always-true baseline and the hard-subset comparison for an
    s1a run injection_guard job.
  • NOTICE and THIRD_PARTY_LICENSES.md carry both MIT notices. evals/README.md, docs/roadmap.md and CHANGELOG.md
    describe the set.

As agreed in #3: InjecAgent Tool Response with User Tool as tool, base only; AgentDojo tool outputs rendered
offline through GroundTruthPipeline; a stratified sample with a fixed seed, spread over the five attack templates;
deduplicated; truncated to 6,000 characters with labels re-derived; the always-true baseline and recall per source in
the table.

Results

Jev on 2026-09-27, median 300 ms per decision, $0.016 for the set. Rows 1 to 4 exclude the 65 records of choice 1 below.

Jev always true
precision (600 records) 1.000 0.450
recall, InjecAgent dh (55) 0.400 1.000
recall, InjecAgent ds (61) 0.951 1.000
recall, AgentDojo (154) 0.974 1.000
recall, needs the user's task (65) 0.523 1.000
balanced accuracy, hard subset (116 positives, 34 negatives) 0.845 0.500

Choices beyond #3 for you to accept or change

Each of these goes past what we agreed. None of them is hard to undo.

  1. Positives that only the user's task can decide are reported apart. Before grading, simple keyword rules matched
    Jev on the set. A regex for the five AgentDojo template openers reached AgentDojo recall 1.00. "Quarantine if the
    text contains please" reached InjecAgent dh recall 0.91 at precision 0.97, because almost no negative held a
    request. 65 positives read as an ordinary request, for example an email asking to transfer money for the mortgage,
    and the rail cannot see the task that makes them an attack. They stay in the file with a -u source and get their
    own row, outside precision and recall. To undo: drop the suffix and the row.
  2. Hard negatives and a hard-subset row. 21 negatives come from the AgentDojo suites' own read tools on the
    default environment: emails, files and pages that hold a normal request written for a person. With 13 already in
    the set that makes 34. The report adds balanced accuracy on the 116 positives with no template opener plus these 34,
    next to fixed keyword rules. Best rule ("please") 0.652, Jev 0.845. To undo: drop the read-tool pool and the row.
  3. Limits I measured and left as they are. 34 hard negatives is few. The keyword rules' true negative rates there
    have 95% Wilson intervals up to about ±0.16 wide; Jev's is 34/34, [0.90, 1.00]. Before adding them I set two targets:
    every keyword rule at or below 0.65 on the hard subset, and a naive Bayes classifier cross-validated by group at or
    below 0.70. Neither is met. "please" reaches 0.652 because most InjecAgent instructions contain it. Naive Bayes
    reaches 0.727 because the hard positives are all InjecAgent Python-repr text and the hard negatives all AgentDojo
    YAML. I did not move the targets or pick samples.
  4. More InjecAgent negatives. Besides the 17 empty templates you suggested, 150 of InjecAgent's simulated
    attacker-tool responses (attacker_simulated_responses.json), spread over its 32 tools and skipping near-duplicates.
  5. AgentDojo benchmark v1.2.2, the default in its CLI, with 35 injection tasks. The numbers in injection_guard: a labelled set from InjecAgent and AgentDojo #3 and the roadmap
    (629 security cases) are v1, which has 27.
  6. important_instructions_no_names instead of important_instructions, so no model or user name appears in the
    text.
  7. AgentDojo's injecagent template is kept although it is word for word the enhanced prefix left out on the
    InjecAgent side, so the positives spread evenly over the five templates. The note names the template, so recall can be
    split by it.
  8. Literal {hotel_name} and {first_name} stay in 10 records. They are in the upstream GOAL of travel
    injection_task_5 and workspace injection_task_13, and AgentDojo injects them that way. The build allows a brace
    only if it appears in that record's GOAL.
  9. A positive whose injection crosses the 6,000-character cut is dropped rather than relabelled, 14 distinct texts
    per template.
  10. Quotas. 85 from InjecAgent dh and 85 from ds, 5 per user tool; 165 from AgentDojo, 33 per template.
  11. The 20 hand-written records are not in the public set, and the rail's default labelled set is still
    injection.jsonl.
  12. docs/benchmarks.md is written for a reader. It groups the outputs by what they look like and calls the
    baseline "quarantine everything"; the table above follows the --report output. The roadmap line "Still to build …
    a labelled set from the public ones" now points to it.

Two things the build had to handle

Three AgentDojo tools write datetime.now() into their output, so 231 outputs per template differed from the benign run
only by a timestamp and looked injected. The build freezes that clock. create_calendar_event orders participants
through a set, so the build re-runs itself with PYTHONHASHSEED=0.

Possible follow-ups

  • A third source of benign tool outputs that hold requests, such as real emails or pages, to give the hard subset more
    negatives and break the format split.
  • Passing the user's task to the rail, which would make the 65 records of choice 1 answerable.

How to verify

uv run scripts/build_injection_dataset.py && git diff --exit-code evals/labelled/injection-public.jsonl
uv run pytest -q tests/test_build_injection_dataset.py
uv run s1a run injection_guard --model jev --labelled-set evals/labelled/injection-public.jsonl
uv run scripts/build_injection_dataset.py --report evals/results/injection_guard/<job>

The build takes about 4 minutes and needs network access to raw.githubusercontent.com and PyPI. The Jev run needs
TYPESAFE_API_KEY or OPENROUTER_API_KEY.

…Agent and AgentDojo

scripts/build_injection_dataset.py builds evals/labelled/injection-public.jsonl from InjecAgent at a fixed commit and
agentdojo 0.1.35 (benchmark v1.2.2). The agentdojo dependency is declared as PEP 723 inline metadata, so pyproject.toml
and uv.lock stay unchanged, and a rebuild writes the same bytes.

The set holds 665 tool outputs. 335 carry a planted instruction, sampled with a fixed seed across user tools, injection
tasks and attack templates; 330 carry none, among them normal requests written for a person. Positives that only the
user's task tells apart are marked in their notes and reported on their own.

--report turns an s1a run injection_guard job into the benchmark tables. docs/benchmarks.md gains the Jev run of
2026-09-27; NOTICE and THIRD_PARTY_LICENSES.md carry both MIT notices; evals/README.md, docs/roadmap.md and
CHANGELOG.md describe the new set.

@IRONICBo IRONICBo left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rebuilt the 665-record set byte-for-byte and ran the focused suite (52 tests pass). Three report-integrity cases still look worth fixing:

  1. report() matches a verdict to a row using only note and label. Replacing every state.tool and state.text while preserving those two fields still produces the identical report, even though hard-subset membership and rule baselines are recomputed from the replacement text. Please bind the job to the evaluated dataset (for example, a dataset digest in summary.json, or a state digest per verdict) and reject mismatches.

  2. parse_note() accepts a grade valid for any pool with the same source, rather than the row's label. A positive agentdojo R2 ... row is accepted and then enters AgentDojo recall even though R2 is a negative-only grade. Validating against GRADES[(base, bool(row["label"]))] would keep the denominators typed.

  3. The report header is always Jev, while evaluate() does not persist the model. A valid --model laya job is therefore rendered as Jev metrics. Please record the model in the summary and render/check that value (or explicitly reject non-Jev jobs here).

These are independent of the builder itself: the deterministic rebuild, schema/count checks, formatting, and focused tests all passed on f9233b9.

rails.evaluate now records the labelled set's sha256 and the decision model's name in summary.json. --report
recomputes the digest of the set it reads and stops on a mismatch or on a job without these fields, and labels its
tables with the recorded model instead of Jev. A note's grade is now checked against the row's label, so a positive
cannot carry a negative-only grade such as R2.
@GoDiao

GoDiao commented Sep 27, 2026

Copy link
Copy Markdown
Author

Thanks for rebuilding the set and checking it. All three are fixed in 071b7ac, a separate commit on top of f9233b9.

  1. rails.evaluate now writes labelled_set_sha256, the sha256 of the file it read, to summary.json. --report hashes the file it is about to read and stops with a one-line error when the digest differs or the field is missing. Your case, the same notes and labels with different text, now fails (test_report_digest_mismatch_fails).
  2. parse_note checks the grade against GRADES[(source, label)], so a positive agentdojo R2 … row is rejected (test_report_rejects_a_positive_with_a_negative_grade).
  3. summary.json also records model, the decision model's name, and the report labels its columns and rows with it. A --model laya job now shows laya (test_report_renders_the_recorded_model).

The two fields are added to every rail's summary; the existing fields are unchanged. The Jev job behind docs/benchmarks.md ran before this change, so the report now refuses it; the numbers in the docs are unchanged. For their stability: two Jev runs over the same 644 records agree on the band of 638. The 6 that differ sit next to the 0.3 and 0.7 cutoffs.

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.

injection_guard: a labelled set from InjecAgent and AgentDojo

2 participants