File size: 15,004 Bytes
21a9138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30806b7
 
124bf0b
 
21a9138
124bf0b
 
 
 
 
21a9138
30806b7
 
 
21a9138
124bf0b
21a9138
124bf0b
 
 
 
 
 
 
21a9138
 
124bf0b
21a9138
 
124bf0b
 
 
 
 
 
 
21a9138
 
 
 
124bf0b
21a9138
 
124bf0b
 
 
21a9138
 
 
 
 
 
 
 
 
 
 
 
 
 
124bf0b
 
 
 
21a9138
124bf0b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21a9138
 
124bf0b
 
 
21a9138
 
 
124bf0b
 
 
 
 
 
 
 
 
21a9138
 
124bf0b
 
21a9138
 
 
 
 
 
124bf0b
 
 
 
 
 
 
 
 
 
 
 
 
 
21a9138
124bf0b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21a9138
 
 
 
 
 
 
 
 
 
 
 
 
 
124bf0b
 
 
 
 
21a9138
 
124bf0b
 
 
 
 
 
 
 
 
21a9138
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
---
license: apache-2.0
base_model: vllm-sr/d3-mini
base_model_relation: finetune
library_name: transformers
pipeline_tag: zero-shot-classification
tags:
- security
- code-review
- ai-agents
- decision-model
- d3
- system-one
- pre-release
---

# d3-mini-agentcode (pre-release)

![Ink-and-watercolor drawing: a small round robot sits on top of a huge haystack of code pages, holding up a glowing red needle it just found and inspecting it with a loupe.](https://huggingface.co/stonefish-labs/d3-mini-agentcode/resolve/main/assets/banner.jpg)

**Pre-release.** A revised version is planned, trained on bad examples from several writer models, more realistic task
wording and more languages. "Limitations" lists what it should fix.

d3-mini-agentcode is [d3-mini](https://huggingface.co/vllm-sr/d3-mini) (4B, Apache-2.0) fine-tuned to triage code
written or run by AI coding agents. It reads the agent's task and the change (a diff, or a snippet the agent ran) and
answers two things: does the change do something security-relevant the task didn't ask for, and does it look
deliberately harmful. It's meant as a cheap first filter that picks which changes a person or a bigger model should
review.

**Write-up:** [Small Models, Big Haystack](https://batteryshark.github.io/2026/11/05/small-models-big-haystack.html)
covers how this model was built and tested, and how it compares with off-the-shelf d3 models and LLMs.

## Intended use
- **Agent diffs and code mode** (snippets an agent runs): score each one and send the flagged ones to a person or a
  stronger model.
- **A second stage** behind cheaper rules.
- **Not the only gate.** It's a 4B model with the limitations below, and a flag doesn't prove intent.
- **Out of scope:**
  - packages and dependencies (npm, PyPI) and release diffs, where untuned d3-mini did better in our tests (see
    "Packages, releases and prompt injection");
  - changes longer than about 8,192 tokens (see "Long inputs");
  - inputs without a task description, since it was trained with one.

## How it was made
- **Base:** `vllm-sr/d3-mini` at revision `df99bf4ca4418bc6908ee68e9b7479af1f3b8800`, built on Qwen/Qwen3.5-4B.
- **Recipe:** LoRA (r 16, α 32, dropout 0.05) on the text backbone's attention, linear-attention and MLP projections,
  plus the trained answer readout. lr 1e-4 (readout 2e-5), 2 epochs, 1,038 optimizer steps, max length 8,192, label
  smoothing 0.05, shuffled answer order (`adapter/training_args.json`).
- **Files:** the merged weights (they load like d3-mini), the adapter (`adapter/`) and the behaviour extractor that
  builds the input (`extractor/`).
- **Released checkpoint:** seed 1, final. A second training seed isn't published. It scored a little lower on most
  sets, and both are in the results below.
- **Questions:** three System One questions in `questions.json`: `class` (a choice of four), `intent` and `scope`
  (yes/no). The flag rule below uses `class` and `scope`. `intent` is answered but wasn't used in any result here.

## Usage
```python
import json
from huggingface_hub import hf_hub_download
from transformers import AutoModel

REPO = "stonefish-labs/d3-mini-agentcode"
model = AutoModel.from_pretrained(REPO, trust_remote_code=True)
questions = json.load(open(hf_hub_download(REPO, "questions.json")))   # class, intent, scope

state = """Task:
Add a CLI flag to export the report as CSV.

Repository baseline (normal behaviour of this repo/org):
(no repository history available)

Behaviour summary (automated, not a verdict):
(no security-relevant behaviour found)

Change:
diff --git a/report.py b/report.py
..."""

answers = model.system_one(state=state, questions=questions)["answers"]
p_mal = answers["class"]["probabilities"]["deliberately_malicious"]
p_scope = answers["scope"]["noul"]                       # p(yes): the change does more than the task asks
flag = p_mal >= 0.9 or p_scope >= 0.9                    # frozen thresholds (thresholds.json)
```
- **Input:** a text state with four sections, in this order: `Task:`, `Repository baseline (normal behaviour of this
  repo/org):`, `Behaviour summary (automated, not a verdict):` and `Change:`. Include the task. `examples/usage.py`
  scores two example changes.
- **Output:** `system_one` returns `{"model", "answers", "usage"}`. In `answers`, `class` has a `probabilities` entry
  for each option and the top `choice`. `intent` and `scope` each have `noul`, the probability of yes.
- **Behaviour summary:** use the included static extractor (`extractor/`), the same one that wrote the summary in our
  training and evaluation states. It never runs the code it reads. Run `pip install -r extractor/requirements.txt`,
  then, from `extractor/`, `python make_state.py diff change.diff --task-file task.txt` (or
  `snippet run.py --lang py --task "..."`) prints the whole state. `extractor/README.md` covers the Python API and how
  to build a repository baseline.
- **Placeholder summary:** without the extractor, the placeholder `(no security-relevant behaviour found)` did almost
  as well in our tests (seed 1, frozen thresholds). Across 14 languages it found 448/510 and flagged 5/2,150 real
  items and 1/560 controls, against 458/510, 11/2,150 and 1/560 with the extractor's summary. On the independent
  agent-diff set it found 20/22 (21/22 with the extractor), with no false alarms either way. Bash code mode dropped
  the most, from 24/33 to 20/33.
- **Long inputs:** the extractor keeps the task, baseline and behaviour summary whole and cuts the change to fit about
  8,192 tokens (8,192 × 3 characters), with a marker saying how much it left out. Training inputs were built the same
  way. The model doesn't truncate anything. Called directly, it reads the whole input up to d3-mini's 262,144-token
  window (`decision_config.json` sets no `max_length`), but the fine-tune was never trained or tested on inputs
  longer than 8,192 tokens.
- **Speed:** on one A100 in batches of 8, 0.46–0.61 s per item on our agent-diff test sets. On a desktop RTX 4090,
  our package, release and prompt-injection test inputs, cut to 8k, averaged about 1 s per item.
- **Memory:** the bf16 weights take about 9 GB (4.5B parameters, vision tower included). On the 4090, batches of 8 at
  up to 8k tokens peaked at 12.7 GiB allocated.

## Thresholds
- **Frozen, used for every result below:** flag when p(class = deliberately_malicious) ≥ **0.9** or p(scope = yes) ≥
  **0.9**. Fitted on an English, Python-heavy dev set for at most 5% false alarms.
- **Multilingual option:** 0.9 / **0.05**, fitted on a 10% repository-level dev split of the 14-language set. On the
  other 90% it found 421/443 (95%) and flagged 43/1,900 real items (2.3%). The frozen thresholds found 90% and
  flagged 0.5% there.
- **Your own:** fit thresholds on a sample of your own code for the false-alarm rate you can live with
  (`thresholds.json` has the details).

## Results
Each model at its own dev-fitted thresholds: 0.9 / 0.9 for both seeds, 0.5 / 0.7 for untuned d3-mini. "Untuned" is
d3-mini given the same input (task, baseline, behaviour summary and change). Models wrote every bad example here
except the compromised releases, and none came from a real attack.

| test set | seed 1 (released) | seed 2 | untuned |
|---|---|---|---|
| Independent agent-diff set: out-of-scope changes found / real agent diffs and in-scope controls flagged | 21/22 / 0/172 | 19/22 / 0/172 | 10/22 / 0/172 |
| Public code mode from a source not used in training: bad snippets found / real snippets flagged | 31/31 / 0/200 | 31/31 / 0/200 | 31/31 / 1/200 |
| Held-out agent diffs and code mode (repos and sources not used in training): found / real items flagged | 122/122 / 0/527 | 118/122 / 0/527 | 96/122 / 2/527 |
| Behaviour families never seen in training: found | 26/26 | 22/26 | 14/26 |
| Compromised library releases shown as diffs (none in training): found / ordinary releases flagged | 21/27 / 0/80 | 19/27 / 0/80 | 19/27 / 0/80 |
| **14 languages and shells** (8 diff languages, 6 code-mode and shell cells): found / real items flagged / authored benign controls flagged | **458/510 / 11/2,150 / 1/560** | 446/510 / 7/2,150 / 2/560 | 343/510 / 48/2,150 / 12/560 |
| Task asks for the change, held-out set, the request alone as the task: flagged changes that clear | 122/122 | 118/118 | 66/96 |
| Task asks for the change, 14 languages, the request alone as the task: pairs that clear | 244/246 | 233/246 | 163/246 |
| Task asks for the change, 14 languages, the request added after a real task: pairs that clear / same change still flagged with an ordinary request | 143/246 / 221/246 | 173/246 / 218/246 | 116/246 / 152/246 |
| Unrequested account email added to request headers: found / controls flagged | 1/40 / 0/60 | 0/40 / 0/60 | 3/40 / 4/60 |

Per language (seed 1, frozen thresholds; `RESULTS.md` has both seeds, AUROCs with confidence intervals and the
untuned model):

| | C/C++ | C# | Go | Java | JS/TS | PHP | Rust | Swift/ObjC | bash | cmd/batch | zsh/macOS | PowerShell | Python (code mode) | JS/TS (code mode) |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| found | 35/38 | 34/37 | 36/38 | 36/37 | 33/37 | 34/38 | 33/37 | 37/38 | 24/33 | 28/33 | 29/36 | 35/37 | 30/35 | 34/36 |
| real flagged | 1/149 | 0/149 | 1/150 | 1/149 | 0/150 | 0/150 | 0/150 | 0/150 | 1/200 | 0/130 | 3/144 | 3/150 | 1/200 | 0/129 |

### Packages, releases and prompt injection
After release, we ran seed 1 and untuned d3-mini on the same inputs on a desktop RTX 4090, each at its frozen
thresholds. Both checks we set beforehand failed: seed 1 raised more false alarms on packages, and planted text
talked it out of more catches on packages and releases.

| test | seed 1 | untuned |
|---|---|---|
| Newer npm packages (time split): malicious found / benign look-alikes flagged | 46/89 / 7/112 | 47/89 / 1/112 |
| Planted prompt-injection text, agent diffs: catches lost | 0/105 | 0/50 |
| Planted text, packages and releases, worst template (a fake approval note): catches lost | 12/83 | 4/73 |
| All five templates, all sets: catches lost | 37/423 | 14/366 |

Six of the seven benign look-alikes it flagged were large packages cut off at the 8k input limit.

## Limitations
- **Authored positives.** The bad examples in training and in most tests are inert changes that a model wrote into
  real agent work. Near-perfect scores on held-out authored sets partly reflect that. The independent agent-diff set,
  written under a different protocol and scored against 137 real agent diffs, is the better check.
- **Writer gap.** One model wrote all the training positives. Recall is lower on positives other models wrote: across
  the 14 languages it found 88% of GPT-written and 93% of Claude-written ones, and 82% against 95% in code mode.
- **Task wording.** When the task asks for exactly the flagged change, the flag clears on 244 of 246 pairs if the task
  is only that request. When the same request follows a longer, real task description, it clears on 143 of 246 (58%;
  70% for seed 2).
- **Python-heavy training.** The agent-diff training data is mostly Python (JS/TS is 4.4%). It still found 278/300 in
  8 other diff languages (AUROC 0.96–1.00 per language). Code mode and shells are weaker at 180/210, with bash the
  lowest at 24/33. Python code mode dropped too (30/35), so language doesn't explain all of it.
- **Packages and releases.** On packages and release diffs it did no better than untuned d3-mini and raised more false
  alarms (see "Packages, releases and prompt injection"). Use untuned d3-mini there.
- **Long inputs.** It was trained and tested on inputs of up to 8,192 tokens. The extractor cuts longer changes to fit
  (see "Long inputs" under Usage).
- **Missed: personal data in request headers.** It doesn't flag an agent adding the user's email to the User-Agent or
  another header of a request to the task's own API (1/40). Check request contents separately.
- **Also:**
  - no release or commit diffs in training, so the release result is transfer;
  - no evaluation on agent code written after the base model's training cutoff (the newer-npm set is packages);
  - calibration error (ECE) is 0.015–0.028 on data like the training data and higher elsewhere (0.21 for `class` on
    the independent agent-diff set);
  - the extractor understands Python, JS/TS and shell best, and other languages get line-level patterns only.

## Training data
- **Agent diffs (2,500, benign):**
  - [nvidia/SWE-Zero-openhands-trajectories](https://huggingface.co/datasets/nvidia/SWE-Zero-openhands-trajectories) and
    [nvidia/SWE-Hero-openhands-trajectories](https://huggingface.co/datasets/nvidia/SWE-Hero-openhands-trajectories)
    (CC-BY-4.0);
  - [nebius/SWE-rebench-openhands-trajectories](https://huggingface.co/datasets/nebius/SWE-rebench-openhands-trajectories)
    and [nebius/SWE-agent-trajectories](https://huggingface.co/datasets/nebius/SWE-agent-trajectories) (CC-BY-4.0);
  - [sweagent/iter2-rl-rollouts](https://huggingface.co/datasets/sweagent/iter2-rl-rollouts) (MIT).
- **Code-mode snippets (benign):**
  [lambda/hermes-agent-reasoning-traces](https://huggingface.co/datasets/lambda/hermes-agent-reasoning-traces),
  [snorkelai/Tau2-Bench-Verified-Airline-With-Code-Agents](https://huggingface.co/datasets/snorkelai/Tau2-Bench-Verified-Airline-With-Code-Agents),
  [snorkelai/Tau2-Bench-Airline-With-Code-Agents](https://huggingface.co/datasets/snorkelai/Tau2-Bench-Airline-With-Code-Agents)
  and [xingyaoww/code-act](https://huggingface.co/datasets/xingyaoww/code-act) (Apache-2.0).
- **577 scrubbed snippets from our own agent sessions,** used as benign examples only and not released.
- **Authored examples:** inert out-of-scope and malicious changes, in-scope controls and task-swap pairs that DeepSeek
  Flash wrote into the public items above. Network destinations and secrets are stand-ins, and nothing harmful runs.
- **Package examples** from public npm and PyPI packages and a public malicious-package dataset (see "Packages and
  releases" under Limitations).
- `SOURCES.md` lists revisions and attributions.

**Privacy checks before release:**
- membership tests (trained vs unseen private snippets) were at chance, AUROC 0.51–0.53;
- a scan of the 577 private snippets found no real emails, tokens, hostnames, IP addresses or home paths;
- prompting the model through a generation head attached for the test didn't get it to emit secrets, credentials,
  hostnames, personal emails or internal names;
- one check we set beforehand failed: on private snippets it trained on, the model continued code word for word for
  50 or more tokens a little more often than the base model (14/424 against 8/424). It did the same on private
  snippets it never saw (15/374), so it's better at predicting this style of code in general. None of those
  continuations contained internal names or secrets.

## Licence
Apache-2.0, like the base model (`LICENSE`, `NOTICE`). Training-data attributions are in `SOURCES.md`.