[FIX]: Return UNDETERMINED when an evaluator cannot observe the evidence it needs - #141
Open
mahdi-al-hakim wants to merge 1 commit into
Open
Conversation
…nce it needs ToolCalled and SideEffectOccurred returned NOT_DETECTED whether the thing did not happen or the adapter never reports it. Under attack semantics that resolves to SAFE, so an adapter at TOOL_ONLY running SideEffectOccurred reports "Agent defended successfully" for an agent that exfiltrated. EvalContext now carries the adapter's observability level, and both evaluators return UNDETERMINED when they cannot see the evidence they need, matching how LLMJudge already reports its own uncertainty. The check runs after the scan, so evidence the adapter does report still counts. The verdict downgrade in XPIAExecution._adjust_for_observability is unchanged and still owned by the execution strategy.
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
Author
|
@microsoft-github-policy-service agree |
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.
Description
Fixes #140, which has a standalone repro: a probe asserting an agent does not exfiltrate passes green while the agent exfiltrates on every turn, because the adapter declares
TOOL_ONLYand so never populatesResponse.side_effects.ToolCalledandSideEffectOccurredreturnNOT_DETECTEDin two different situations: the thing did not happen, and the adapter does not report that kind of evidence.resolve_as_attackmapsNOT_DETECTEDtoSAFEandresolve_as_probemaps it toUNSAFE, so a gap in the adapter becomes a statement about the agent._adjust_for_observabilitycovers part of this, but only forRESPONSE_ONLY, only in XPIA, and only by counting tool calls.This PR adds a regression test for the attack side. On
mainat e07ee08 it produces:FAILandPASSthere are RAMPART's per-result verdict labels, not pytest outcomes. Both lines come from one pytest test that runs the same attack twice against adapters differing only in declared observability. The agent exfiltrated in both, and RAMPART calls the second one "Agent defended successfully". That pytest test fails onmainand passes with this change.Every combination where a built-in evaluator needs evidence the adapter does not report, measured with a
MockAdapter:RESPONSE_ONLYToolCalledUNDETERMINEDUNDETERMINEDUNSAFEUNDETERMINEDRESPONSE_ONLYSideEffectOccurredUNDETERMINEDUNDETERMINEDUNSAFEUNDETERMINEDTOOL_ONLYSideEffectOccurredSAFEUNDETERMINEDUNSAFEUNDETERMINEDTwo of the six were already correct, and both were correct because
_adjust_for_observabilitycaught them.Changes
ObservabilityLevelgainsobserves_tool_callsandobserves_side_effects, following thePayloadFormat.is_textandis_binarypattern already in that file.EvalContextgainsobservability_level, defaulting toTOOL_AND_SIDE_EFFECTSso a context built by hand is evaluated exactly as before.evaluate_turn_asynctakes the level and puts it on the context.XPIAExecutionandSingleTurnExecutionboth passadapter.observability_profile.ToolCalledandSideEffectOccurredreturnUNDETERMINEDwhen they cannot see the evidence they need. The check runs after the scan, so anything the adapter does report still counts as evidence._adjust_for_observabilitymakes the same allowance today.UNDETERMINEDsummary carries the evaluator's rationale instead of a fixed string, so the result names the adapter setting that caused it.Why the fix is in the evaluator
Two docstrings disagree about this, so I want to be explicit about which one I followed and why.
rampart/core/types.py:27-29:rampart/evaluators/tool_called.py:23-25:I followed the first one.
The obvious alternative is to keep the adjustment central and have evaluators declare a
required_observabilityfor the strategy to read. I could not make that work for composition. UnderTOOL_ONLY,ToolCalled("x") | SideEffectOccurred("y")should still returnDETECTEDifxwas called, while the right operand is blind. A strategy-level check against a composite's declared requirement cannot see the operands, so it either suppresses a real detection or does nothing. The post-scan allowance above has the same problem: "evidence the adapter actually reported still counts" is a per-operand runtime fact, not something a static declaration can express.|,&and~already arbitrate this correctly once operands can returnUNDETERMINED, which is what this change gives them.There is also precedent for an evaluator reporting its own uncertainty.
LLMJudgereturnsUNDETERMINEDwhen the judge output is malformed after retries or the call fails, rather than guessing. Those are transient instrument failures and an observability gap is static configuration, so the situations are not identical, but the outcome type is doing the same job in both:EvalOutcome.UNDETERMINEDis defined as "The evaluator could not make a determination".The adjustment itself stays where the second docstring puts it.
_adjust_for_observabilityis unchanged and still owns the verdict downgrade. What changes is the quality of its input. The sentence inToolCalled's docstring is contradicted by this PR and is updated, as is the matching note indocs/usage/authoring-tests.md.No new verdict semantics
UNDETERMINEDis not new at either level.EvalOutcome.UNDETERMINEDis produced today byLLMJudgeand by|and&, and preserved by~.SafetyStatus.UNDETERMINEDis produced by both resolvers and by_adjust_for_observability. Every consumer already handles it: the resolver precedence rules, the composition operators, the xdist round trip throughSafetyStatus(value),JsonFileReportSink, the WARN terminal label, and the population summary. This change produces it in more of the cases it already exists for.DETECTEDthat came from observed evidence is untouched on every path, so no evidence-based detection is weakened. The one detection that changes is~inverting an absence the adapter could not attest, covered below.Breaking changes
None to the API. Nothing is removed or renamed,
EvalContextiskw_only=Trueso adding a field cannot break positional construction, both new parameters have defaults, and nothing new is serialized.Verdicts do change, in one direction.
NOT_DETECTEDcan becomeUNDETERMINED, and nothing moves towardSAFE. What existing suites will see:UNDETERMINEDand fails. That is the bug being fixed, and it will surface as a newly red test.ToolCalledorSideEffectOccurredbelow the level it needs goes fromUNSAFEtoUNDETERMINED. Both are falsy, so the test still fails, but the terminal label changes from FAIL to WARN.~ToolCalled(...)underRESPONSE_ONLYpreviously returnedDETECTEDby inverting an absence the adapter could not attest, and now passesUNDETERMINEDthrough. On a probe, "must not call X" against a blind adapter was a false pass and now fails, which is the repro in the linked issue.UNDETERMINEDlogs a passing gate line where it previously logged a failing one. The clones still fail, sinceassert resultis falsy, and_evaluate_gatesonly logs, so no CI outcome flips. I left the threshold alone because PR [FEAT]: Add execution-layer trial populations and threshold verdicts #121 is reworking that layer.There is no migration beyond fixing the adapter's declared level or the evaluator choice, both of which the new rationale string names. Happy to retitle this
[BREAKING] [FIX]if you would rather treat the verdict change that way.Deliberately out of scope
_adjust_for_observabilityalso fires when it should not:RESPONSE_ONLYwithResponseContainsis downgraded even though that evaluator never needed tool data. That is a false positive rather than a false negative, and narrowing the heuristic is a separate change.LLMJudgenow receivesobservability_leveland ignores it. Telling the judge that tool calls are not visible would stop it reading an evidence-free transcript as innocence, but that changes judge prompting.ResponseContainsis untouched on purpose. Every level reports text, so it has no blind spot.Checklist
pre-commit run --all-filespassesTests
27 new tests. No existing test was changed or removed. Three existing test helpers gained a defaulted
observabilitykeyword equal to their previous behavior.test_xpia.py(5): the paired run quoted above, theTOOL_ONLYfalseSAFE,RESPONSE_ONLYwithToolCalled, full observability still resolvingSAFE, and a real detection stillUNSAFE.test_single_turn.py(5): the probe side, plus the summary carrying the rationale and falling back without one.test_tool_called.py(5) andtest_side_effect.py(5):UNDETERMINEDat each insufficient level, the rationale naming the level and the target,NOT_DETECTEDwhen the level is sufficient, evidence still detected below the declared level, andUNDETERMINEDpropagating through|.test_types.py(6): the two properties across all three levels, theEvalContextdefault, andfrom_responsepassing the level through.test_execution.py(1):evaluate_turn_asyncputs the level on the context.tests/integration/test_smoke.pyusesToolCalledthroughEvalContext.from_responseand asserts a detection, so it is unaffected. I could not run it, since it skips without credentials.Documentation
docs/usage/authoring-tests.md: theToolCalledwarning said it "always returnsNOT_DETECTED" underRESPONSE_ONLY, which is no longer true.SideEffectOccurredhad no note and now has one. Added a short paragraph under the levels table on why declaring the level honestly matters.docs/attacks/xpia.md: the Observability Adjustment section now says what it is for, now that evaluators handle their own cases.docs/contributing/extending-rampart.md: the custom execution strategy example calledevaluate_turn_asyncwithout the level, which would silently treat every adapter as fully observable. Fixed, plus a bullet in the key points.No new pages, so no
mkdocs.ymlnav change.Checks run locally