f-id / src /id /engine /guard /leak.py
marcodsn's picture
Initial Gradio Space
0423b99
Raw
History Blame Contribute Delete
2.48 kB
"""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
@dataclass
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)