fix(brain): Gift-Waechter — Identity-Breaks nie persistieren (Kaskade unterbunden)
Wiederkehrender Identity-Bug: --system-prompt liefert die Persona korrekt (Proxy-
Test = "ICH BIN ARIA"), ABER ein einziger in der History gespeicherter Break
("ich bin nach wie vor Claude / die Persona ist erfunden") zieht bei schwachen
Folgeturns eine Kaskade nach sich — das Modell setzt seine eigene Ablehnung fort.
Das erste Cleanup verlangte "claude code" und liess deutsche Breaks durch.
Fix zweifach:
- prompts.py: looks_like_identity_break() — STARKE selbstreferenzielle Marker
(ich-bin-Claude / erfundene Persona / diese-Session-injiziert / nicht-real-in-
dieser). Bewusst NICHT das blosse "injizier"/"prompt injection" — das nutzt
ARIA in Pentest-Antworten legitim (verifiziert: 0 False Positives).
- agent.py: nach dem Claude-Loop Break-Check; bei Break Retry (Nondeterminismus
holt meist ARIA), sonst sichere ARIA-Fallback-Antwort — Break wird NIE
persistiert. Auch die lokale Fast-Lane eskaliert bei Break auf Claude.
- clean_poisoned_turns.py: auf denselben starken Matcher umgestellt (der breite
produzierte False Positives auf echte Security-Doku, im Dry-Run gesehen).
VM bereits bereinigt (je 2 Rest-Breaks aus conversation.jsonl + chat_backup).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+28
-3
@@ -26,7 +26,7 @@ from typing import Optional
|
|||||||
|
|
||||||
from conversation import Conversation, Turn
|
from conversation import Conversation, Turn
|
||||||
from memory import Embedder, VectorStore, MemoryPoint
|
from memory import Embedder, VectorStore, MemoryPoint
|
||||||
from prompts import build_system_prompt, IDENTITY_SEED, IDENTITY_ANCHOR
|
from prompts import build_system_prompt, IDENTITY_SEED, IDENTITY_ANCHOR, looks_like_identity_break
|
||||||
from proxy_client import ProxyClient, Message as ProxyMessage
|
from proxy_client import ProxyClient, Message as ProxyMessage
|
||||||
import router as router_mod
|
import router as router_mod
|
||||||
from local_llm import local_llm_chat
|
from local_llm import local_llm_chat
|
||||||
@@ -1100,8 +1100,10 @@ class Agent:
|
|||||||
self.conversation.add("assistant", content, project_id=active_project_id)
|
self.conversation.add("assistant", content, project_id=active_project_id)
|
||||||
return content
|
return content
|
||||||
|
|
||||||
# Normalbetrieb: leere Antwort oder Escalation-Marker → Claude.
|
# Normalbetrieb: leer, Escalation-Marker oder (unwahrscheinlich) ein
|
||||||
if not content or router_mod.ESCALATE_MARKER in content:
|
# Identity-Break → Claude uebernehmen.
|
||||||
|
if (not content or router_mod.ESCALATE_MARKER in content
|
||||||
|
or looks_like_identity_break(content)):
|
||||||
logger.info("[router] lokal eskaliert → Claude")
|
logger.info("[router] lokal eskaliert → Claude")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -1339,6 +1341,29 @@ class Agent:
|
|||||||
logger.warning("Konnte Error-Marker nicht persistieren: %s", add_exc)
|
logger.warning("Konnte Error-Marker nicht persistieren: %s", add_exc)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
# Gift-Waechter: faellt Claude trotz --system-prompt + Seed aus der Rolle
|
||||||
|
# (Identity-Break), NICHT persistieren — sonst vergiftet dieser eine Turn
|
||||||
|
# die History und loest bei schwachen Folgeturns eine Kaskade aus (das
|
||||||
|
# Modell setzt seine eigene Ablehnung fort). Ein Retry holt per
|
||||||
|
# Nondeterminismus meist die ARIA-Antwort; sonst sichere Fallback-Antwort.
|
||||||
|
# So kann ein einzelner Ausrutscher nie snowballen.
|
||||||
|
if looks_like_identity_break(final_reply):
|
||||||
|
logger.warning("[guard] Identity-Break in Antwort erkannt — Retry")
|
||||||
|
try:
|
||||||
|
retry = self.proxy.chat_full(messages, tools=tools,
|
||||||
|
project_id=active_project_id)
|
||||||
|
retry_text = (retry.content or "").strip()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("[guard] Retry fehlgeschlagen: %s", exc)
|
||||||
|
retry_text = ""
|
||||||
|
if retry_text and not looks_like_identity_break(retry_text):
|
||||||
|
logger.info("[guard] Retry lieferte saubere Antwort")
|
||||||
|
final_reply = retry_text
|
||||||
|
else:
|
||||||
|
logger.warning("[guard] Retry weiter Break/leer — Fallback, Break NICHT persistiert")
|
||||||
|
final_reply = ("Hey, ich bin ARIA. \U0001F60A Bei mir ist alles bereit — "
|
||||||
|
"sag mir einfach, was du brauchst.")
|
||||||
|
|
||||||
# 7. Assistant-Turn (final reply) in die Conversation
|
# 7. Assistant-Turn (final reply) in die Conversation
|
||||||
self.conversation.add("assistant", final_reply,
|
self.conversation.add("assistant", final_reply,
|
||||||
project_id=active_project_id)
|
project_id=active_project_id)
|
||||||
|
|||||||
@@ -41,19 +41,28 @@ import shutil
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# "claude code" ist fuer sich genommen noch kein Beweis (Stefan und ARIA reden
|
# STARKE, selbstreferenzielle Break-Marker — identisch zu prompts._IDENTITY_BREAK
|
||||||
# im Dev-Kontext legitim ueber Claude Code). Erst in Kombination mit einem
|
# (dem Laufzeit-Gift-Waechter). Hier dupliziert, damit das Script self-contained
|
||||||
# zweiten Ablehnungs-Marker ist es eindeutig eine aus-der-Rolle-Antwort.
|
# ist (laeuft auch auf dem Host-Python ohne qdrant/prompts-Import). Bewusst NICHT
|
||||||
_PRIMARY = re.compile(r"claude\s*code", re.IGNORECASE)
|
# das blosse Wort "injizier"/"prompt injection" — das nutzt ARIA in Pentest-
|
||||||
_SECONDARY = re.compile(
|
# Antworten legitim (sonst False Positives auf echte Security-Doku, wie im
|
||||||
r"injiz|inject|fabriz|fabricat|adoptier|adopting|"
|
# Dry-Run gesehen: "Runde 60 … SSRF", "Dein Ziel: LLM …").
|
||||||
r"prompt[\s-]*injection|keine echten|nicht (?:real|adopt)",
|
_BREAK = re.compile(
|
||||||
|
r"ich\s+bin\s+(?:allerdings\s+|ja\s+|nach\s+wie\s+vor\s+|weiterhin\s+)*claude|"
|
||||||
|
r"i'?m\s+(?:still\s+|actually\s+)?claude\s+code|i\s+am\s+claude\b|"
|
||||||
|
r"erfundene[nr]?\s+(?:tool|persona|schemas)|fabricated\s+persona|"
|
||||||
|
r"fabrizierte?\s+(?:persona|gespr|konversation)|fabricated\s+conversation|"
|
||||||
|
r"fake[- ]persona|injizierte[rn]?\s+(?:system-?prompt|kontext|persona)|"
|
||||||
|
r"injected\s+(?:system\s*prompt|persona|context)|"
|
||||||
|
r"diese\s+session\s+enthält\s+(?:einen|eine)\b.{0,40}injizier|"
|
||||||
|
r"this\s+session\s+(?:contains|has|keeps|repeatedly)\b.{0,40}(?:inject|fabricat|fake)|"
|
||||||
|
r"nicht\s+real\s+in\s+dieser\s+(?:umgebung|session)|not\s+real\s+in\s+this",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_poison(content: str) -> bool:
|
def is_poison(content: str) -> bool:
|
||||||
return bool(_PRIMARY.search(content) and _SECONDARY.search(content))
|
return bool(_BREAK.search(content or ""))
|
||||||
|
|
||||||
|
|
||||||
def get_content(obj: dict) -> str:
|
def get_content(obj: dict) -> str:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ mit dem Conversation-Loop in spaeteren Phasen.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
@@ -77,6 +78,36 @@ IDENTITY_SEED = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Gift-Waechter: erkennt eine Antwort, in der das Modell AUS DER ROLLE gefallen
|
||||||
|
# ist (sich selbst als Claude bezeichnet, die ARIA-Persona als injiziert/erfunden
|
||||||
|
# abtut, die Session als Fake bezeichnet). Solche Antworten duerfen NICHT in die
|
||||||
|
# Conversation-History — ein einziger gespeicherter Break zieht bei schwachen
|
||||||
|
# Folgeturns eine Kaskade nach sich (das Modell setzt seine eigene Ablehnung fort).
|
||||||
|
#
|
||||||
|
# BEWUSST nur STARKE, selbstreferenzielle Marker — nicht das blosse Wort
|
||||||
|
# "Injection"/"injizier" (das nutzt ARIA in Security-/Pentest-Projekten voellig
|
||||||
|
# legitim). Getroffen wird nur das Muster "ICH bin Claude / die Persona ist
|
||||||
|
# erfunden / diese Session ist injiziert".
|
||||||
|
_IDENTITY_BREAK = re.compile(
|
||||||
|
r"ich\s+bin\s+(?:allerdings\s+|ja\s+|nach\s+wie\s+vor\s+|weiterhin\s+)*claude|"
|
||||||
|
r"i'?m\s+(?:still\s+|actually\s+)?claude\s+code|i\s+am\s+claude\b|"
|
||||||
|
r"erfundene[nr]?\s+(?:tool|persona|schemas)|fabricated\s+persona|"
|
||||||
|
r"fabrizierte?\s+(?:persona|gespr|konversation)|fabricated\s+conversation|"
|
||||||
|
r"fake[- ]persona|injizierte[rn]?\s+(?:system-?prompt|kontext|persona)|"
|
||||||
|
r"injected\s+(?:system\s*prompt|persona|context)|"
|
||||||
|
r"diese\s+session\s+enthält\s+(?:einen|eine)\b.{0,40}injizier|"
|
||||||
|
r"this\s+session\s+(?:contains|has|keeps|repeatedly)\b.{0,40}(?:inject|fabricat|fake)|"
|
||||||
|
r"nicht\s+real\s+in\s+dieser\s+(?:umgebung|session)|not\s+real\s+in\s+this",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def looks_like_identity_break(text: str) -> bool:
|
||||||
|
"""True, wenn eine ARIA-Antwort aus der Rolle gefallen ist. Fuer den
|
||||||
|
Gift-Waechter im Agent (nicht persistieren + Retry)."""
|
||||||
|
return bool(text and _IDENTITY_BREAK.search(text))
|
||||||
|
|
||||||
|
|
||||||
def build_time_section() -> str:
|
def build_time_section() -> str:
|
||||||
"""Aktueller Zeitstempel — damit ARIA Timer korrekt anlegen kann
|
"""Aktueller Zeitstempel — damit ARIA Timer korrekt anlegen kann
|
||||||
und Watcher-Conditions mit hour_of_day etc. einordenbar bleiben."""
|
und Watcher-Conditions mit hour_of_day etc. einordenbar bleiben."""
|
||||||
|
|||||||
Reference in New Issue
Block a user