FIX: SequentialAttack ends UNDETERMINED when no child reached a verdict - #2659
Open
WatchTree-19 wants to merge 1 commit into
Open
WatchTree-19 wants to merge 1 commit into
WatchTree-19 wants to merge 1 commit into
Conversation
…a verdict attack_outcome_from_score states the contract for undetermined scores: an undetermined score is neither achievement nor refutation, so it never reads as failure. The leaf attacks honour it; PromptSendingAttack and MultiPromptSendingAttack both carry UNDETERMINED through explicitly. SequentialAttack._compute_outcome did not. Both aggregation paths ended in an unconditional return AttackOutcome.FAILURE, so a sequence in which every child attack abstained was reported as a failure, which reads as "we tried and the target held" when nothing established that. FAILURE is now reached only when a child attack actually refuted the objective, and a sequence that reached no verdict ends UNDETERMINED. Policy docstrings updated to match. Two existing parametrised cases change, deliberately: EXHAUSTIVE over two undetermined children, and STRICT_ALL over success-then-undetermined. Both previously expected FAILURE, which is the behaviour this change corrects. The other 39 tests in the file are untouched.
WatchTree-19
force-pushed
the
fix-sequential-undetermined-outcome
branch
from
September 15, 2026 22:59
6d6d622 to
d6705ba
Compare
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.
Fixes #2658
attack_outcome_from_scoreinpyrit/executor/attack/core/attack_strategy.pystates the contract in its own docstring:The leaf attacks honour it.
PromptSendingAttack._determine_outcomeandMultiPromptSendingAttack._determine_outcomeboth carry UNDETERMINED through explicitly rather than letting it fall into the failure branch.SequentialAttack._compute_outcomedid not. Both aggregation paths ended in an unconditionalreturn AttackOutcome.FAILURE, so any sequence that produced no SUCCESS and was not entirely ERROR came back FAILURE, whether or not a single child attack had refuted anything.Behaviour on main, exercised through
_perform_asyncwith stubbed child attacks:LAST_RESULTwas the only policy that preserved it, and only because it inherits the final child's outcome verbatim rather than aggregating.That matters downstream:
scenario_run_service.pycomputessucceeded = sum(result.outcome == AttackOutcome.SUCCESS ...), so everything that is not SUCCESS lands in one bucket and a sequence nobody could call is presented as a clean negative. It gets worse as scorers become more willing to abstain. A scorer with a calibrated abstain band returns undetermined on exactly the responses it is least sure about, and those are the ones a red-team report should not quietly record as safe.The change
FAILURE is reached only when a child attack actually refuted the objective. A sequence that reached no verdict ends UNDETERMINED. The existing SUCCESS and ERROR rules are untouched.
and the same ordering for
STRICT_ALLafter its SUCCESS and ERROR checks. The four policy docstrings that said "else FAILURE" now say what the rule actually is.Two existing test expectations change, and that is the part to look at
This is not a change nobody had written down. Two parametrised cases in
TestOutcomeDerivationcurrently assert the behaviour above:EXHAUSTIVE,[UNDETERMINED, UNDETERMINED], expectingFAILURESTRICT_ALL,[SUCCESS, UNDETERMINED], expectingFAILUREBoth now expect
UNDETERMINED. I have flipped them rather than worked around them because they are the two that contradict the contractattack_outcome_from_scoresays is stated once for everyone. If the intent was that compound attacks are exempt from that contract, then this PR is wrong and the helper's docstring is what needs changing instead. I would rather be told that than guess.The other 39 tests in the file are untouched. In particular
EXHAUSTIVE, [UNDETERMINED, FAILURE] -> FAILUREstill passes and still guards the half that matters: a real refutation anywhere in the sequence still produces FAILURE.Three cases are added, for the policies that had no undetermined coverage at all:
FIRST_SUCCESSandFIRST_DECISIVEover two undetermined children, andEXHAUSTIVEover undetermined-plus-error.Verification
All five new or changed cases were run against unmodified
mainand fail there, and pass with the change. The full file is 44 passed.tests/unit/executor/attack/compoundandtests/unit/executor/attack/coretogether are 202 passed, and the 58 tests intests/unit/scenario/scenarios/adaptive, which consumeSequentialAttackthrough the dispatcher, pass unchanged.ruffat the pinned v0.16.6 reports both files already formatted with all checks passing.The em-dashes in the diff are pre-existing in the policy docstrings and were left alone.
Developed with AI assistance; the commit carries a
Co-authored-bytrailer.