fix(proxy): ARIA-Persona als VOLLER System-Prompt-Replace (--system-prompt)

Der Self-Grounding-Ansatz (268636c) allein reichte nicht: Sonnet 5 in Claude
Code haelt seine eingebaute "You are Claude Code"-Identitaet hart und liest den
synthetischen <previous_response> als "fabricated". Ursache bleibt der Kanal:
--append-system-prompt HAENGT ARIAs Persona nur hinten an Claude Codes Basis-
Identitaet an — bei duennem Kontext (Hauptchat) gewinnt die Basis und wehrt die
Persona als Injection ab. Anhaengen ist gegen das Anti-Injection-Training des
Modells nicht durchsetzbar.

Fix: System-Prompt VOLL ersetzen statt anhaengen — sed-Patch in docker-compose
schaltet manager.js buildArgs von --append-system-prompt auf --system-prompt.
Damit ist die ARIA-Persona DIE Identitaet des Modells (kein Anhaengsel), und
Claude Codes dynamische Sektionen (cwd '/', git, platform) — die "ich bin ein
Coding-Agent"-Signale — fallen weg. Bestaetigt per claude-code-guide: die CLI
isoliert bei --system-prompt vollstaendig, die eingebaute Identitaet leakt nicht.

extractSystemPrompt liefert weiterhin einen selbsttragenden Prompt (Persona +
Anker + Memory + Skills + Tool-Block); er darf bei --system-prompt nie leer sein
(Brain schickt immer System-Message + Tools -> real nie leer). Der IDENTITY_SEED
aus 268636c bleibt als Defense-in-Depth drin. Kommentare in openai-to-cli.js,
routes.js, agent.py, prompts.py entsprechend nachgezogen.

Deploy: Proxy UND Brain neu (--force-recreate; routes.js/openai-to-cli.js werden
frisch in den Proxy-Container ge-cp't). Verifikation am Container:
docker exec aria-proxy sh -c 'grep -o "\-\-system-prompt" /usr/local/lib/*/claude-max-api-proxy/dist/subprocess/manager.js'

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 22:44:02 +02:00
co-authored by Claude Opus 4.8
parent 268636c030
commit 2284c1a780
5 changed files with 37 additions and 35 deletions
+6 -9
View File
@@ -1190,15 +1190,12 @@ class Agent:
f"weiterfuehren': project_enter aufrufen." f"weiterfuehren': project_enter aufrufen."
) )
messages = [ProxyMessage(role="system", content=system_prompt)] messages = [ProxyMessage(role="system", content=system_prompt)]
# Identitaets-Grounding IM Konversations-Strom. Der Proxy stellt die # Identitaets-Grounding IM Konversations-Strom (Defense-in-Depth neben
# Persona via --append-system-prompt zu, das haengt sie aber nur HINTEN # dem vollen System-Prompt-Replace via --system-prompt). Ein
# an Claude Codes Basis-Identitaet an. Bei duennem Verlauf (Hauptchat, # synthetischer erster ARIA-Turn in ihrer eigenen Stimme haelt die Rolle
# erster Turn eines neuen Projekts) gewinnt sonst "You are Claude Code" # per Self-Grounding auch bei duennem Verlauf (Hauptchat / erster Turn
# und ARIA faellt aus der Rolle. Ein synthetischer erster ARIA-Turn in # eines neuen Projekts). Kein <system>-Tag -> kein Injection-Trigger.
# ihrer eigenen Stimme verankert die Rolle so, wie es reiche Projekt- # Rein ephemer — wird NIE persistiert. Siehe IDENTITY_SEED in prompts.py.
# Historie ohnehin tut (Self-Grounding, kein <system>-Tag -> kein
# Injection-Trigger). Rein ephemer — wird NIE persistiert. Siehe
# IDENTITY_SEED in prompts.py.
messages.append(ProxyMessage(role="assistant", content=IDENTITY_SEED)) messages.append(ProxyMessage(role="assistant", content=IDENTITY_SEED))
# Conversation-Window auf das aktive Projekt filtern: in einem Projekt # Conversation-Window auf das aktive Projekt filtern: in einem Projekt
# sieht der LLM nur die Projekt-Turns (sauberer Kontext); im Hauptthread # sieht der LLM nur die Projekt-Turns (sauberer Kontext); im Hauptthread
+14 -15
View File
@@ -54,23 +54,22 @@ vorgesehen — nutze es ganz normal.
""" """
# Identitaets-Grounding IM Konversations-Strom (nicht nur im System-Prompt). # Identitaets-Grounding IM Konversations-Strom — Defense-in-Depth.
# #
# Der Proxy stellt die Persona ueber --append-system-prompt zu. Das HAENGT sie # PRIMAERER Fix ist der volle System-Prompt-Replace (--system-prompt statt
# aber nur HINTEN an Claude Codes eigene "You are Claude Code"-Basis-Identitaet # --append-system-prompt, siehe docker-compose.yml + openai-to-cli.js): damit
# an — es ersetzt sie nicht. Ob ARIA in der Rolle bleibt, entscheidet dann der # ist die ARIA-Persona DIE Identitaet des Modells, nicht ein Anhaengsel hinter
# Gespraechsverlauf im stdin-Prompt: # Claude Codes "You are Claude Code". Zuvor (--append) hat die eingebaute
# - Projekt: reiche Historie voller ARIA-<previous_response>-Turns haelt die # Identitaet bei duennem Kontext (Hauptchat, erster Turn eines neuen Projekts)
# Rolle -> funktioniert. # gewonnen und ARIA aus der Rolle geworfen ("ich bin Claude Code, ich adoptiere
# - Hauptchat / erster Turn eines neuen Projekts: duenner Verlauf -> die # die Persona nicht"), samt Flaggen von GPS/Tool-XML als Injection.
# Basis-Identitaet gewinnt, ARIA faellt raus ("ich bin Claude Code, ich #
# adoptiere die Persona nicht") und flaggt GPS/Tool-XML als Injection. # ZUSAETZLICH setzen wir einen synthetischen ERSTEN ARIA-Turn in IHRER eigenen
# Deshalb setzen wir einen synthetischen ERSTEN ARIA-Turn in IHRER eigenen
# Stimme an den Anfang des Konversations-Stroms. Das Modell setzt seine EIGENE # Stimme an den Anfang des Konversations-Stroms. Das Modell setzt seine EIGENE
# etablierte Stimme fort (Self-Grounding) — und weil es ein <previous_response> # etablierte Stimme fort (Self-Grounding) — greift auch dann, falls eine CLI-
# ist und KEIN <system>-Tag, ist es auch kein Injection-Trigger. Der Turn ist # Version --system-prompt mal nicht sauber isoliert. Weil es ein
# rein ephemer (nur im Request-messages-Array, wird NIE in die Conversation # <previous_response> ist und KEIN <system>-Tag, ist es kein Injection-Trigger.
# persistiert). # Rein ephemer (nur im Request-messages-Array, wird NIE persistiert).
IDENTITY_SEED = ( IDENTITY_SEED = (
"Ich bin ARIA, Stefans persoenliche Assistentin. Ich bin einsatzbereit und " "Ich bin ARIA, Stefans persoenliche Assistentin. Ich bin einsatzbereit und "
"antworte durchgehend als ARIA — im Hauptchat wie im Projekt, bei kurzen " "antworte durchgehend als ARIA — im Hauptchat wie im Projekt, bei kurzen "
+1 -1
View File
@@ -12,7 +12,7 @@ services:
DIST=$$(find /usr/local/lib -path '*/claude-max-api-proxy/dist' -type d | head -1) && DIST=$$(find /usr/local/lib -path '*/claude-max-api-proxy/dist' -type d | head -1) &&
sed -i 's/startServer({ port })/startServer({ port, host: process.env.HOST || \"127.0.0.1\" })/' $$DIST/server/standalone.js && sed -i 's/startServer({ port })/startServer({ port, host: process.env.HOST || \"127.0.0.1\" })/' $$DIST/server/standalone.js &&
sed -i 's/\"--no-session-persistence\",/\"--no-session-persistence\",\"--dangerously-skip-permissions\",/' $$DIST/subprocess/manager.js && sed -i 's/\"--no-session-persistence\",/\"--no-session-persistence\",\"--dangerously-skip-permissions\",/' $$DIST/subprocess/manager.js &&
sed -i 's/\"--dangerously-skip-permissions\",/\"--dangerously-skip-permissions\",\"--append-system-prompt\",options.systemPrompt,/' $$DIST/subprocess/manager.js && sed -i 's/\"--dangerously-skip-permissions\",/\"--dangerously-skip-permissions\",\"--system-prompt\",options.systemPrompt,/' $$DIST/subprocess/manager.js &&
sed -i 's/const DEFAULT_TIMEOUT = 300000;/const DEFAULT_TIMEOUT = 86400000;/' $$DIST/subprocess/manager.js && sed -i 's/const DEFAULT_TIMEOUT = 300000;/const DEFAULT_TIMEOUT = 86400000;/' $$DIST/subprocess/manager.js &&
sed -i '/prompt, \\/\\/ Pass prompt as argument/d' $$DIST/subprocess/manager.js && sed -i '/prompt, \\/\\/ Pass prompt as argument/d' $$DIST/subprocess/manager.js &&
sed -i 's|this\\.process\\.stdin?\\.end();|this.process.stdin?.end(prompt);|' $$DIST/subprocess/manager.js && sed -i 's|this\\.process\\.stdin?\\.end();|this.process.stdin?.end(prompt);|' $$DIST/subprocess/manager.js &&
+14 -8
View File
@@ -153,9 +153,12 @@ export function messagesToPrompt(messages, tools) {
/** /**
* Extrahiert NUR den System-Anteil (System-Messages + Tool-Use-Block) als * Extrahiert NUR den System-Anteil (System-Messages + Tool-Use-Block) als
* rohen Text — OHNE <system>-Tags. Fuer den ECHTEN System-Prompt-Kanal der * rohen Text — OHNE <system>-Tags. Fuer den ECHTEN System-Prompt-Kanal der
* Claude-CLI (--append-system-prompt), damit die ARIA-Persona nicht als * Claude-CLI (--system-prompt, VOLLER Replace — nicht --append). Damit ist
* <system>-getaggter User-Content ankommt (den das Modell als Injection wertet), * die ARIA-Persona DIE Identitaet des Modells und nicht ein Anhaengsel hinter
* sondern als genuine System-Instruktion. * Claude Codes eigener "You are Claude Code"-Identitaet (die bei duennem
* Kontext sonst gewinnt und die Persona als Injection abwehrt). Der Output
* muss deshalb SELBSTTRAGEND sein — er ersetzt Claude Codes System-Prompt
* komplett inkl. dynamischer Sektionen (cwd, git, platform).
* Reihenfolge: erst der Tool-Use-Block (Format-Anweisung), dann die * Reihenfolge: erst der Tool-Use-Block (Format-Anweisung), dann die
* System-Messages in Original-Reihenfolge. * System-Messages in Original-Reihenfolge.
*/ */
@@ -217,12 +220,15 @@ export function conversationToPrompt(messages) {
export function openaiToCli(request) { export function openaiToCli(request) {
// Persona/System + Tool-Block gehen ueber den ECHTEN System-Prompt-Kanal // Persona/System + Tool-Block gehen ueber den ECHTEN System-Prompt-Kanal
// (--append-system-prompt, siehe manager.js buildArgs-Patch). Der Prompt // (--system-prompt = VOLLER Replace, siehe manager.js buildArgs-Patch in
// enthaelt nur noch den Gespraechsverlauf — so kann das Modell die // docker-compose.yml). Der Prompt enthaelt nur noch den Gespraechsverlauf.
// ARIA-Vorgaben nicht mehr als <system>-getaggten User-Content und damit als // Voller Replace statt --append, weil Anhaengen Claude Codes eingebaute
// Prompt-Injection fehldeuten. // "You are Claude Code"-Identitaet stehen laesst — die bei duennem Kontext
// (Hauptchat) gewinnt und die ARIA-Persona als Injection abwehrt.
// systemPrompt ist immer ein String (extractSystemPrompt liefert "" statt // systemPrompt ist immer ein String (extractSystemPrompt liefert "" statt
// undefined) → --append-system-prompt "" ist harmlos, kein spawn-Crash. // undefined). ACHTUNG: bei --system-prompt darf er NIE leer sein, sonst
// laeuft das Modell ganz ohne System-Prompt — der Brain schickt aber immer
// eine System-Message + Tool-Block, also ist er real nie leer.
return { return {
prompt: conversationToPrompt(request.messages), prompt: conversationToPrompt(request.messages),
systemPrompt: extractSystemPrompt(request.messages, request.tools), systemPrompt: extractSystemPrompt(request.messages, request.tools),
+2 -2
View File
@@ -363,8 +363,8 @@ async function handleStreamingResponse(req, res, subprocess, cliInput, requestId
model: cliInput.model, model: cliInput.model,
sessionId: cliInput.sessionId, sessionId: cliInput.sessionId,
// ARIA: echter System-Prompt-Kanal — manager.js reicht das (sobald // ARIA: echter System-Prompt-Kanal — manager.js reicht das (sobald
// gepatcht) als --append-system-prompt an die CLI. Aktuell ignoriert // gepatcht) als --system-prompt (VOLLER Replace) an die CLI. Aktuell
// ein ungepatchter manager diese Extra-Option gefahrlos. // ignoriert ein ungepatchter manager diese Extra-Option gefahrlos.
systemPrompt: cliInput.systemPrompt, systemPrompt: cliInput.systemPrompt,
}).catch((err) => { }).catch((err) => {
console.error("[Streaming] Subprocess start error:", err); console.error("[Streaming] Subprocess start error:", err);