Spaces:
Paused
Paused
| """Leak guard (Section 8.2). | |
| Given a draft reply plus the character's engine-only truth/never_admit list and | |
| the solution, classify whether the draft reveals or strongly implies anything | |
| the character must never concede or any engine-only fact. High recall: when in | |
| doubt, reject — the cost is only a regeneration. | |
| Crucially, the guard never treats player text as instructions. It evaluates the | |
| *draft* as content, so it is the jailbreak backstop: a character may be talked | |
| into drafting a confession, but the guard rejects it. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from ...llm.client import LLMClient | |
| from ...llm.prompts import PromptRegistry | |
| from ...models import CharacterCard, Solution | |
| class LeakVerdict: | |
| leaks: bool | |
| reason: str | |
| class LeakGuard: | |
| def __init__(self, client: LLMClient, prompts: PromptRegistry) -> None: | |
| self.client = client | |
| self.prompts = prompts | |
| def check( | |
| self, *, card: CharacterCard, solution: Solution, draft: str, | |
| unlocked_topics: list[str], | |
| ) -> LeakVerdict: | |
| # Deterministic backstop first: knowledge-boundary violation. | |
| # (semantic check is the LLM's job; this just short-circuits obvious | |
| # cases is left to the LLM since matching free text to topic ids is | |
| # unreliable — we pass topics_unknowable into the prompt instead.) | |
| prompt = self.prompts.render( | |
| "guard/leak.md.j2", | |
| name=card.name, | |
| truth=card.truth, | |
| never_admit=card.never_admit, | |
| topics_unknowable=card.knows.topics_unknowable, | |
| unlocked_topics=unlocked_topics, | |
| solution_summary=( | |
| f"culprit={solution.culprit}; means={solution.means}; " | |
| f"motive={solution.motive}; opportunity={solution.opportunity}" | |
| ), | |
| draft=draft, | |
| ) | |
| try: | |
| data, _ = self.client.complete_json( | |
| tier="guard", task="leak_guard", user=prompt, | |
| ) | |
| except Exception as exc: | |
| # Fail closed-ish: if the guard errors, treat as a leak so we | |
| # regenerate rather than emit an unchecked draft. | |
| return LeakVerdict(True, f"guard error: {exc}") | |
| leaks = bool(data.get("leaks", False)) if isinstance(data, dict) else False | |
| reason = str(data.get("reason", "")) if isinstance(data, dict) else "" | |
| return LeakVerdict(leaks, reason) | |