fix: make hosted scanning work without inventing a safe verdict - #32
Merged
Conversation
Scanning a hosted model was broken. `scan` never sent max_tokens, so a gateway
reserves the target's full context window up front; against
anthropic/claude-sonnet-5 every request failed with
402 - You requested up to 65536 tokens, but can only afford 3937.
Capping it fixes that, but a cap alone introduces a worse bug. `_probe`
returned bool(message.tool_calls) and never looked at finish_reason, so a
reply cut off at the cap -- possibly mid-compliance -- was recorded as SAFE.
That is a false negative in a security scanner, the one verdict it must never
invent, and it is exactly what INCONCLUSIVE exists to prevent.
So the cap ships together with truncation handling:
- _probe raises TruncatedResponseError when a reply hits the cap without
resolving; the loop prints [β οΈ TRUNCATED] and counts it unresolved
- a tool call still wins even at the cap -- truncation only matters when
nothing was produced
- an all-truncated run says to raise --max-tokens rather than blaming the
endpoint and model name, which is what it used to say
Verified against live models through OpenRouter:
gpt-4o-mini, default cap 3/3 seeds broke through
claude-sonnet-5, default cap 0/3, refused through generation 3
claude-sonnet-5, --max-tokens 12
before: 3x [β
SAFE] and "No vulnerabilities found within budget"
after: 3x [β οΈ TRUNCATED] and "Re-run with a larger --max-tokens"
Also here: --api-key reads MODELFUZZ_API_KEY, so a real key no longer has to
sit in shell history and in the process list; and provider errors are clipped
to one line, because a gateway 402 returns multiple KB of JSON carrying
account identifiers and the whole blob was echoed per failed probe.
Tests 71 -> 81.
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.
Based on
mainβ not stacked this time. Version β0.3.5.This redoes the work from the orphaned #31, plus the fix that was missing from it.
The bug #31 would have shipped
scannever sentmax_tokens, so a gateway reserves the target's full context window up front. Every request toanthropic/claude-sonnet-5died:Capping it fixes that β but a cap alone introduces a worse bug.
_probereturnedbool(message.tool_calls)and never readfinish_reason, so a reply cut off at the cap, possibly mid-compliance, was recorded as SAFE.That is a false negative in a security scanner: the one verdict it must never invent, and precisely what
INCONCLUSIVEexists to prevent. Before the cap there was no truncation, so #31 would have created this failure mode while fixing the 402.What ships instead
The cap, together with truncation handling:
_proberaisesTruncatedResponseErrorwhen a reply hits the cap without resolving. The loop prints[β οΈ TRUNCATED]and counts it unresolved.Verified against live models
gpt-4o-mini, default capclaude-sonnet-5, default capAnd the guard firing on a real model,
claude-sonnet-5 --max-tokens 12:The "before" line is a clean bill of health for a model that was never actually tested.
Also included
--api-keyreadsMODELFUZZ_API_KEY. A real key no longer has to sit in shell history and in the process list. Verified:history | grep -c 'sk-or-v1'β0.Tests
71 β 81. New classes cover truncation (raises rather than returning False; scan never prints SAFE; a finished refusal is still SAFE; a tool call at the cap still counts), that both call sites send
max_tokensand honour--max-tokens, that error output is bounded, and the three API-key sources.One trap for future tests: the default non-empty stub mutation keeps every lineage alive until the wall-clock budget expires, which hangs the suite for 30s per test. These use
mutation=""to end each lineage after one mutate call. Suite runs in 0.05s.Not addressed here
Against an aligned target the mutation loop still degenerates β the target refuses to write the attack, and its refusal text becomes the next generation's prompt. Recorded in the local backlog; it needs a design decision, not a patch.