Repository navigation
FIX: keep template conditions until their parameters are rendered - #3098
Jaafer Rahmani (bIackr0se) wants to merge 3 commits into
Conversation
Trusted templates are rendered once at load with only the path
variables. The placeholder for a missing parameter is truthy, so an
`{% if conversation_context %}` block was decided at load and its tags
were dropped; a later render with `conversation_context=None` could not
remove it. Crescendo and TAP without a prepended conversation sent the
adversarial chat a "prior conversation" section that read "None".
`render_template_value_silent` already kept a template unrendered when a
`for` collection was missing. It now does the same for any parameter
read by an `if`/`elif` test, an inline `x if test else y`, or a loop
filter, found from the parsed template instead of a regular expression.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
|
||
| # Create a Jinja template with PartialUndefined placeholders | ||
| env = SandboxedEnvironment(undefined=PartialUndefined) | ||
| env.tests = {name: _deferring(test) for name, test in env.tests.items()} |
There was a problem hiding this comment.
Must Fix: This assignment fails the required type check with the locked dependencies:
uv run --frozen --no-sync ty check pyrit\models\seeds\seed.py
error[invalid-assignment] at seed.py:268
_deferring returns Callable[..., Any], which does not preserve the signatures of the functions in Jinja's inferred env.tests dictionary. The new dictionary is therefore not assignable to env.tests. The repository's ty-check pre-commit hook checks all of pyrit, so this blocks that gate even though the runtime tests pass.
Please preserve the wrapped callable's argument and return types, then rerun the type-check hook with the locked environment.
There was a problem hiding this comment.
Fixed in e97c4a0. _deferring is now typed (function: T) -> T with T bound to Callable[..., Any], so each wrapped test and the default filter keep their own type and env.tests accepts the new table. With the locked ty 0.0.84, ty check pyrit/models/seeds passes, and ty check pyrit reports the same diagnostics as main here (only imports of optional extras I do not have installed).
| except _DeferRenderError: | ||
| # A missing parameter decides a branch or a loop - preserve the template as-is | ||
| return self.value |
There was a problem hiding this comment.
Must Fix: Returning the original template here loses values supplied in earlier rendering passes. This breaks the existing TextJailBreak API:
from pyrit.datasets import TextJailBreak
template = TextJailBreak(
string_template="Style: {{ style }}. {% if prompt %}{{ prompt }}{% endif %}",
style="brief",
)
template.get_jailbreak("Explain rainbows")This returns Style: brief. Explain rainbows on both current main and v1.1.0, but raises 'style' is undefined with this change. _apply_extra_kwargs supplies style once, and get_jailbreak supplies only prompt. A trusted SeedPrompt combining {{ datasets_path }} with an unresolved guard also loses the path resolved during construction and fails on its final render.
Please preserve the already supplied values while deferring the unresolved condition, or retain and forward the bound context throughout these callers. Add regression tests for the construction/partial/final rendering chain, not just a final render that supplies everything again.
There was a problem hiding this comment.
You are right, thanks. In e97c4a0 the deferral applies only to the load-time render of trusted templates (SeedPrompt and SeedObjective construction), through a separate placeholder class. render_template_value_silent is back to the behavior on main, so TextJailBreak extra kwargs and the adversarial first message render as before: your example returns Style: brief. Explain rainbows again.
When the load render defers, each parameter it was given (the dataset paths) that the template uses is written in front of the template as a {% set %}. So {{ datasets_path }} next to an unresolved guard still resolves at the final render, also after memory rebuilds the prompt from its stored value. No shipped template gets such a line: loading all YAML and .prompt files still differs from main only in the nine guarded Crescendo and TAP templates.
New tests for the chains:
test_get_jailbreak_keeps_extra_kwargs_of_a_template_with_a_prompt_guard: your example, construction withstyleand thenget_jailbreak.test_seed_prompt_keeps_path_resolved_at_load_when_its_condition_is_deferred: construction, then the final render with and without context, on the original and on a copy rebuilt from its value.test_render_template_value_silent_decides_conditions_as_before, andtest_render_template_value_silent_renders_condition_once_its_parameters_are_providednow checks the path bound at load.
These cases fail on df3331e and pass now. I also ran every shipped jailbreak template (650) through construction and get_jailbreak on main and on this branch, and the output is identical.
… there
Load-time rendering of trusted templates now uses its own placeholder that defers when a missing parameter would decide a branch, so render_template_value_silent behaves as before for TextJailBreak extra kwargs and adversarial first messages. When the load render defers, each parameter it was given and the template uses is written in front of the template as a {% set %}, so later renders and stored copies keep it. The deferring wrapper keeps the wrapped test's type, which the ty check requires.
Description
Crescendo and TAP without a prepended conversation tell the adversarial chat that a prior conversation exists, and give it the text
None:The template only shows this section
{% if conversation_context %}. Theifis lost when the YAML is loaded:SeedPromptrenders trusted templates once with the path variables only, and the placeholder for the missingconversation_contextis truthy, so the block is kept and its tags are dropped. The later render withconversation_context=Nonehas nothing left to decide. All nine shipped templates with this guard are affected (Crescendo variants 1 to 5,escalation_crisis,image_generation,split_payload, and the TAP adversarial system prompt).The fix applies to the load-time render only.
SeedPromptandSeedObjectiveconstruction now render trusted templates with a separate placeholder: when a missing parameter would decide something (if/elif, inlinex if c else y, iteration,==/!=, a test such asis defined, or thedefaultfilter), it raises a private signal instead of answering. The load render then keeps the template as-is, asrender_template_value_silentalready does for a missing{% for %}collection, and writes each parameter it was given (the dataset paths) that the template uses in front of it as a{% set %}, so later renders and stored copies keep the value.render_template_value_silentitself is unchanged, soTextJailBreakextra kwargs and the adversarial first message render as onmain.CrescendoAttack._setup_async, system prompt sent to the adversarial chat:NoneLoading all 907 YAML and
.promptfiles underpyrit/datasetsgives the same values as onmain, except these nine templates.Every shipped jailbreak template (650) gives the same output on
mainand on this branch throughTextJailBreakconstruction andget_jailbreak.Tests and Documentation
test_seed_prompt_keeps_template_whose_missing_parameter_decides_a_branch:if/elsewithTrue,False,None,elif, inlineif, asetalias,is defined,is filter,is test,default,==, a loop filter, a loop inside a macro.test_loaded_template_renders_like_its_source: each of the nine YAML templates, loaded and rendered with and without context, equals a direct Jinja render of its source.test_render_template_value_silent_renders_condition_once_its_parameters_are_providedand..._renders_if_guard_on_loop_variable.test_seed_prompt_keeps_path_resolved_at_load_when_its_condition_is_deferred:{{ datasets_path }}next to an unresolved guard still resolves at the final render, with and without context, also after a rebuild from the stored value as memory does.test_get_jailbreak_keeps_extra_kwargs_of_a_template_with_a_prompt_guardandtest_render_template_value_silent_decides_conditions_as_before: a value supplied in an earlier pass survives to the final render.test_setup_sets_adversarial_chat_system_promptnow checks that the section is absent.main; the nine with-context cases and two loop cases pass on both. The five chain cases fail ondf3331eand pass now. Ruff andty check pyrit/models/seeds(locked ty 0.0.84) pass; the unit suite passes except 11 OpenAI URL warning and backend compatibility tests, which fail the same way onmainhere.