FIX Preserve converter role scoping in prepended history - #2359
FIX Preserve converter role scoping in prepended history#2359Roman Lutz (romanlutz) wants to merge 27 commits into
Conversation
Default prepended request conversion to user history and restrict Jailbreak composition to explicitly compatible direct techniques. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Integrate the original request converter scoping fix on current main while preserving its history. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Apply request converters to role-separated prepended history before flattening, while preventing the resulting request from being converted twice. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Explain which factories opt in, what callers append, and why constructor support alone does not guarantee safe converter composition. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Fail clearly when role-scoped converters produce non-text prepended history that string normalization cannot preserve. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Reuse prepared non-chat requests across retries and preserve converter piece indexes when applying role-scoped prepended conversion. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Limit inline jailbreak-template conversion to the scenario-owned prompt_sending delivery. Keep native system-prompt delivery separate and remove the now-unnecessary cross-technique composition capability. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Integrate the latest mainline changes while preserving the converter-scoping branch history and behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Restore the exact origin/main blob after the merge's line-ending check normalized this unrelated file. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Document why non-chat history is converted before flattening, why original and wire views stay separate, and how retry and piece-index safeguards work. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Build the reusable Bijection technique foundation on top of PR microsoft#2359's request-converter scoping and non-chat flattening behavior. Remove the parallel convert_new_user_prompts_only flag and express Bijection-style prepended-context preservation through PrependedConversationConfig(apply_converters_to_roles=[]). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Richard Lundeen (rlundeen2)
left a comment
There was a problem hiding this comment.
Overall I think we can make this a lot smaller. The actual bug is narrow: request converters can rewrite simulated assistant history when an attack supplies a prepended conversation. It's easy to hit with context_compliance, where the jailbreak's TextJailbreakConverter ends up rewriting the fake assistant turns. So to me this is about which messages we convert, not new machinery — and I don't think we need to touch ConversationManager or put state on Message.
Here's the direction I'd like instead:
- PrependedConversationConfig stays the sole policy for prepended history. It already picks which roles get converters (
apply_converters_to_roles) and which normalizer flattens history. If we default that to["user"], the assistant-history bug is basically fixed on its own — assistant history is simulated target output, so you have to opt in with["assistant"]to touch it. If we ever need more control over prepended history, we add it here. - StrategyConverterConfig gets a way to scope a converter to the first turn. This decides which messages a converter applies to, exactly like
apply_converters_to_rolesalready does. And because it's declarative it rides in the attack identifier, so resume/eval stay deterministic for free. - Optional implementation detail We can derive "first turn" straight from memory if we want — without threading anything new through the attacks. The normalizer already has the
conversation_idand a memory handle, so it can just ask: has the target actually replied yet? Define first-turn as "no realassistantmessage in this conversation" (prepended turns are stored assimulated_assistant, so they don't count). The nice thing is every case falls out correctly, including the tricky ones — e.g. on aprompt_sendingretry we spin up a fresh conversation, so the jailbreak is correctly re-applied, whereas a naive per-send counter would drop it. Multi-turn continuations (Crescendo, TAP nodes copied from a parent) already have a real assistant reply, so they're correctly not re-wrapped. - Jailbreak scenario: I still like
prompt_sendingandjailbreak_system_promptas the defaults. We could add one or two more to show off the scoping (e.g. role play with the jailbreak on the system prompt, orcontext_compliancewith the jailbreak only on the first live turn). But I don't think we should just include all techniques (like how they were before this PR) — they're all a bit different, and the jailbreak locus isn't the same for each. If we do role play, is the jailbreak on the system prompt, the first turn, or every turn? That should be a deliberate choice per technique, which is exactly why the scope belongs on the config.
Net: ConversationManager keeps applying the converter list it's given (role-filtered), no eager conversion, and no _request_converters_applied marker on Message. For non-chat targets we go back to today's behavior — flatten history, prepend it, let the live send convert the block. Unless I'm missing something, that's the whole change.
|
Richard Lundeen (@rlundeen2) I like The proposal would work if converter scope were applied while the conversation was still structured, before target normalization/flattening. That requires either the bridge in this PR or a larger pipeline change that introduces structured history adaptation and applies origin/role/turn scope before normalization. So I think |
|
Good catch, I think ConversationManager existed before target capabilities and it shouldn't do message normalization anymore. Agreed with the issue with ConversationManager flattening things. But I don't agree with this: "a larger pipeline change that introduces structured history adaptation" because it's already built and wired. I think the following would be tighter/less error-prone and not significantly more complicated than the current approach of adding converters to the conversation manager.
So the shape I'd like to land on:
One wrinkle is multi turn (but not editable) targets like playwright will need to squash but not squash previous turns. But that's the idea that Also worth noting: |
|
But this means normalization happens in the target, and history represents something different, right? History may show 8 turns of prompt/response but it's perhaps a non-editable history target so everything gets squashed into a single prompt. History should reflect that. Unless I'm misunderstanding. |
Persist prepended conversations structurally for every target, then adapt them only for the first live request when editable history is unavailable. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Replace shared formatter registration with an explicit one-shot target normalization context. Keep prepended history structured in memory, stage role-scoped conversion before persistence, and adapt history with the first live request immediately before provider invocation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Create a fresh target normalization context when single-turn target rotation carries system messages into a new conversation. This keeps the next request single-message while retaining its system framing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Use HistorySquashNormalizer for both one-shot prepended-history adaptation and ordinary single-turn capability adaptation, with shared multimodal handling and documented lifecycle scopes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 420eef57-7a1c-4dde-8aad-a93539e8da64
Use a real PromptTarget test double so Bijection foundation tests exercise PR microsoft#2359's target normalization context instead of inspecting pre-normalized mock send arguments. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 420eef57-7a1c-4dde-8aad-a93539e8da64
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep cloned and retained stateless TAP branches pinned to their original prepended seed, correlate failed exchanges using guarded persisted adjacency, and make normalization ownership explicit. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: cb50fd66-f0e0-4435-a71d-1d42fef7d307
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 420eef57-7a1c-4dde-8aad-a93539e8da64 (cherry picked from commit 3d0ba4e)
Add complete annotations to the new regression tests and synchronize the paired message-normalizer notebook without dropping outputs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6776aa95-0924-4967-a056-e5a228af3faa
Description
Request converters could rewrite simulated-assistant history when an attack supplied a prepended conversation. This was especially easy to trigger when AIRT Jailbreak attached a template converter to conversation-based techniques such as
context_compliance.This change makes prepended conversion role-aware while preserving the logical conversation:
usermessages. Assistant and simulated-assistant conversion require explicit opt-in.AttackStrategyowns one resolvedPrependedConversationConfig, shared normalizer resolution, and identifier provenance. Single-turn and multi-turn base classes carry that policy so concrete attacks only forward it, including SkeletonKey and factory-created attacks.EDITABLE_HISTORYnormalizer override. The default target capability policy remains unchanged, includingMULTI_TURN: RAISEbehavior and native guardrails such as Crescendo's editable-history requirement.TargetNormalizationContextrecords the exact persisted seed-message IDs and provider-attempt lifecycle. It does not infer the seed from message roles or successful responses.processingandunknownfailures and their genuinely adjacent request are excluded from replay.blockedandemptyremain real provider turns.Why capability overrides and an explicit context
This incorporates rlundeen2's proposal to express editable-history adaptation through the target capability pipeline, while retaining explicit state for the part capabilities cannot answer.
A target capability tells us what adaptation is required. It does not tell us:
Inferring those answers from memory is ambiguous. A
blockedoremptyresponse is still a real provider round trip, a write-only target may not persist an assistant response, and a processing error may otherwise be mistaken for seed history.TargetNormalizationContexttherefore owns only the explicit seed boundary and send lifecycle.PrependedConversationConfigsupplies the formatter as a per-send capability override, and the target's normalization pipeline owns the provider-facing adaptation. Centralizing that policy onAttackStrategykeeps the same configuration and identifier visible across base classes, concrete techniques, and factory construction instead of rebuilding it in each attack. This provides:The same shared
HistorySquashNormalizerimplements both prepended-history adaptation and ordinary multi-turn adaptation. Lifecycle belongs to the surrounding context and capability pipeline, not to a second flattening implementation.Processing order
EDITABLE_HISTORYoverride.PromptNormalizer.A pre-provider failure leaves the seed available for retry. Once provider invocation begins, a stateful conversation consumes its seed even if the call later fails or is cancelled. A duplicated logical conversation remaps the seed IDs and starts unconsumed.
Tests and documentation
Coverage includes centralized attack-policy ownership and factory forwarding, SkeletonKey, PAIR, BargeIn, final-assistant score selection, target normalization and lifecycle, real PromptNormalizer error persistence, stateful and stateless payloads, retries and cancellation, concurrent sends, TAP retained and cloned branches, non-text converter output and placeholder warnings, custom capability policies, Crescendo requirements, configured formatters, multimodal requests, converter/factory/Jailbreak compatibility, and latest-main integration. Framework and target documentation describe component ownership, capability distinctions, processing order, branching behavior, metadata ownership, and the direct-streaming BargeIn exception.