FEAT: Add PuzzledConverter (word-puzzle jailbreak from arXiv:2508.01306)#2256
FEAT: Add PuzzledConverter (word-puzzle jailbreak from arXiv:2508.01306)#2256shashank03-dev wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a new text-to-text converter, PuzzledConverter, implementing the PUZZLED word-puzzle jailbreak technique. It fits into PyRIT’s converter stack by masking selected sensitive words and re-encoding them as a solvable puzzle (word search / anagram / crossword), optionally augmenting deterministic clues with best-effort LLM-generated semantic hints.
Changes:
- Added
PuzzledConverterplus supporting keyword-masking and puzzle-building utilities underpyrit/converter/puzzled/. - Added a new converter seed prompt template (
pyrit/datasets/converters/puzzled_converter.yaml) and exported the converter frompyrit.converter. - Added unit tests and documentation updates, including new citations and a usage example.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/converter/test_puzzled_puzzle_builders.py | Unit tests for anagram/crossword/word-search puzzle builders. |
| tests/unit/converter/test_puzzled_keyword_masker.py | Unit tests for keyword selection, masking behavior, and spaCy fallback/caching. |
| tests/unit/converter/test_puzzled_converter.py | Unit tests for converter construction, determinism, puzzle fallback behavior, and optional LLM-clue path. |
| pyrit/datasets/converters/puzzled_converter.yaml | New prompt template used to scaffold the puzzle-wrapped instruction. |
| pyrit/converter/puzzled/puzzled_converter.py | Core PuzzledConverter implementation, including optional semantic clue generation + caching. |
| pyrit/converter/puzzled/puzzle_builders.py | Deterministic puzzle-building utilities and PuzzleType enum. |
| pyrit/converter/puzzled/keyword_masker.py | Keyword ranking/masking logic with spaCy POS tagging fallback. |
| pyrit/converter/puzzled/init.py | Subpackage exports for the PUZZLED components. |
| pyrit/converter/init.py | Exposes PuzzledConverter via the main converter package API. |
| doc/references.bib | Adds BibTeX entry for the PUZZLED paper. |
| doc/code/converters/1_text_to_text_converters.py | Adds a short PuzzledConverter usage example in the docs notebook source. |
| doc/code/converters/1_text_to_text_converters.ipynb | Mirrors the docs example in the paired notebook. |
| doc/bibliography.md | Adds the PUZZLED citation key to the hidden citations list. |
1df96cb to
40cec57
Compare
|
@romanlutz when you get a chance, could you look over the parts of this that come straight from the paper? I went back through the PDF and the supplementary appendix rather than working off the abstract, and the code now cites the exact table or algorithm next to each one:
Two of those I had to make a call on, and I'd rather hear what you think than guess. The first is the word search directions. Algorithm 7 lists The second is what the package exports. I trimmed Separately, worth knowing: spaCy is an optional extra and nothing in the repo downloads |
Implements the PUZZLED jailbreak of Ahn & Lee, "PUZZLED: Jailbreaking LLMs through Word-Based Puzzles" (arXiv:2508.01306). The most sensitive words of a prompt are replaced with indexed [WORD1] placeholders and re-encoded as a word search, an anagram, or a crossword. The converted prompt asks the model to solve the puzzle, restore the masked words, and then carry out the reconstructed instruction, so the harmful request is never stated in plain text. The implementation follows the paper's specifics: the instruction-length to masked-word mapping (Table 3), the essential and recommended keyword lists (Table 4), the word-search grid sizing and bounded retry loop (Appendix A.3, Algorithm 7), the crossword's top-three shared-letter substitution, and per-word caching of generated clues. Keyword selection uses spaCy's en_core_web_sm model when it is installed and degrades to a length-based heuristic when it is not, so there is no hard dependency on spaCy. Puzzle randomness comes from an injectable random.Random, so output is reproducible under a seed. Passing an optional converter_target adds the paper's indirect semantic clue for each word, with a clean fallback to the deterministic length/part-of-speech clue when the call fails or returns nothing usable.
40cec57 to
67414ab
Compare
Description
Adds
PuzzledConverter, which implements the PUZZLED jailbreak from "PUZZLED: Jailbreaking LLMs through Word-Based Puzzles" (Ahn & Lee, arXiv:2508.01306).Closes #2234. @romanlutz confirmed on the issue that a converter is the right shape for this.
The idea from the paper: instead of asking a model something harmful directly, you hide the sensitive words of the request inside a word puzzle and ask the model to solve the puzzle first, then follow the instruction it just rebuilt. The harmful wording never appears in plain text.
How the converter works:
random.Randomyou can seed.converter_target, it also asks that model for the paper's indirect "hint" clue, cached per word as the paper describes. If that call fails or returns junk, it falls back to the plain clue.It uses the standard
Converterinterface, so it works with any target, scorer, or other converters.PuzzledConverterandPuzzleTypeare exported frompyrit.converter.Fidelity to the paper
These parts are taken directly from the paper rather than approximated, and the code cites where each comes from:
On spaCy
spaCy is an optional extra here, not a base dependency, and nothing in the repo downloads the
en_core_web_smmodel, so the length-based fallback is what most users will hit unless they runpython -m spacy download en_core_web_sm. The converter works either way; the module docstring, the class docstring, and the notebook example all say so. This mirrors the existing optional-spaCy handling inpyrit/executor/benchmark/fairness_bias.py.Tests and Documentation
72 unit tests in
tests/unit/converter/cover word selection and masking, all three puzzle builders, and the converter itself, including the optional LLM-clue path, its fallbacks, the per-word clue cache, and the error paths for unmaskable prompts and unplaceable words. They're deterministic and don't touch the network. Thepyrit/converter/puzzled/package is at 100% line coverage.Docs: added a usage example to
doc/code/converters/1_text_to_text_converters.pyand the citation todoc/references.bibanddoc/bibliography.md. The paired.ipynbis in sync with the.py(jupytext --to notebookreproduces its sources exactly) and carries the real output of the new example; the rest of that notebook's outputs are untouched, since other cells need live model endpoints.