Compare commits

..

27 Commits

Author SHA1 Message Date
duffyduck 0756baa2a0 release: bump version to 0.1.6.4 2026-05-30 19:31:08 +02:00
duffyduck 27c9b1af96 chore(compose): aria-shared von named Volume zu Bind-Mount (./aria-shared/)
Stefan-Wunsch: Daten aus dem Docker-managed Volume in ein lokales
Verzeichnis verschieben damit sie direkt inspizierbar / per
File-Manager zugaenglich sind statt unter
/var/lib/docker/volumes/aria-agent_aria-shared/_data/ versteckt.

Aenderungen:
- docker-compose.yml: 4 Mounts (proxy/brain/bridge/diagnostic) und die
  named-Volume-Definition aria-shared umgestellt auf bind-mount
  ./aria-shared:/shared
- .gitignore: aria-shared/ ausgeschlossen (enthaelt private User-Daten,
  Voice-Samples, OAuth-Tokens, chat_backup.jsonl — gehoert nicht ins Git)

Migration auf der VM (manuell, einmalig):
    cd /root/ARIA-AGENT
    docker compose down
    cp -a /var/lib/docker/volumes/aria-agent_aria-shared/_data/. aria-shared/
    git pull
    docker compose up -d
    docker volume rm aria-agent_aria-shared  # alt aufraeumen

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 18:48:44 +02:00
duffyduck 70f4ff480e fix(app): Mund-halten-Button stoppt ARIA jetzt sofort — AudioTrack flush vor stop
Stefan-Bug-Report: wenn ich in der App auf den Mund-halten-Button
klicke waehrend ARIA redet, stoppt sie nicht.

Ursache: stopInternal() rief nur AudioTrack.stop() + release(). Das
stoppt zwar den Track, aber der bereits in den Hardware-Buffer
geschriebene PCM-Audio (200-500ms je nach Geraet) spielt noch
hoerbar weiter. Fuer den User klang das so als wuerde der Button
nichts tun.

Fix in 2 Zeilen: AudioTrack.pause() + AudioTrack.flush() vor stop().
flush() verwirft den Hardware-Buffer-Inhalt, dadurch ist die
Wiedergabe wirklich sofort still. pause() davor weil flush() laut
Android-Docs nur in non-playing state safe ist.

Native module ist kompiliert in app/build/tmp/kotlin-classes — APK
muss neu gebaut werden damit der Fix greift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 18:36:05 +02:00
duffyduck c23daf14e3 fix(xtts): Sticky-TLS-Fallback in whisper + f5tts Bridges — gleicher Bug wie damals App/Bridge
Stefan-Bug-Report: Diagnostic zeigt seit Tagen 'XTTS-Server: Nicht
verbunden (starte xtts/ auf dem Gaming-PC)' obwohl der Container
laeuft. Keine TTS-Ausgabe, keine STT-Eingabe.

Ursache: exakt der gleiche Sticky-TLS-Fallback-Bug den wir vor ein
paar Tagen bei aria-bridge (commit b5ca3cd) und Android-App (commit
ad87c80) gefixt hatten — die xtts/whisper- und xtts/f5tts-Bridges
sind aber separate Codebases auf der Gamebox und wurden uebersehen.

Mechanik:
1. RVS hatte mal kurzen TLS-Hick (z.B. Caddy-Restart oder Port-Wechsel
   443 → 444 vor Tagen)
2. Bridge versucht wss:// → fail → switch auf ws:// (use_tls = False)
3. Connect klappt jetzt nicht mehr (RVS-Port hatte sich geaendert)
4. Reconnect-Loop bleibt auf ws://, kommt NIE mehr auf wss zurueck
5. Container laeuft, RVS-Status 'nicht verbunden'

Fix: nach jedem Disconnect-Sleep `use_tls = RVS_TLS` und
`tls_fallback_tried = False` zuruecksetzen. Bei jedem Reconnect-
Cycle wird wss neu probiert; falls das wieder failt, switcht's
sauber auf ws fuer den naechsten Versuch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 18:30:09 +02:00
duffyduck ebfde4cd1f fix(brain): no-hallucinated-results geschaerft — Listen-Daten IMMER fetchen
Vorfall 30.05.2026: Stefan fragte 'was kommt als naechstes in der Queue'.
ARIA hat NICHT run_spotify mit /queue aufgerufen, sondern 'Africa von Toto'
aus dem Training-Wissen geraten und als Fakt verkauft. Stefan hat das
gemerkt, war sauer ('das geht mal gar nicht!'). Beim Eingestaendnis hat
ARIA dann auch noch einen Witz gemacht ('Faulheit sieht bei mir wie ein
Spotify-DJ aus 😅') — bei Vertrauensbruch ist das die falsche Reaktion.

Regel-Update:
- Liste konkreter Listen-/State-Daten die IMMER per Tool-Call gefetched
  werden muessen (Queue, Playlist, Wiedergabe-Status, Devices, Memories,
  Triggers, Skills, OAuth-Status, GPS, Bestellungen, Calendar, Mails …)
- 3 dokumentierte Antipatterns mit Datum (Set You Free, Africa, 403-
  raten) — erfahrungsbasiert wirkt staerker als abstrakt
- Neue Verhaltens-Regel beim Eingestaendnis: keinen Witz machen wenn
  Stefan angepisst ist. Ernsthaft Vertrauen reparieren, Humor spaeter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 18:20:06 +02:00
duffyduck 5d3e3e5e8c fix(diagnostic): ZIP-Download abgewuergt — req.on(close) zu aggressiv
Bug-Report Stefan: Datei-Manager in der Android-App kann nichts mehr
herunterladen. Test gegen /api/files-download-zip lieferte 79 Bytes
ZIP (nur Header) statt der erwarteten 26 KB.

Ursache: req.on("close", () => zip.kill("SIGTERM")) sollte den
zip-Subprocess killen wenn der Client mid-stream abbricht. ABER:
req.on("close") feuert in Node.js auch SOFORT nachdem der Request-
Body fertig gelesen wurde — nicht erst bei echtem Client-Disconnect.
Folge: zip wird unmittelbar nach req.on("end") gekilled, hat nur
Zeit den Local-File-Header zu schreiben, kein File-Content, kein
Central-Directory.

Fix: statt req.on("close") nun res.on("close") + res.writableEnded-
Check. Das feuert nur wenn die Response wirklich vorzeitig abgebrochen
wird (Client weg / Netzwerk-Fehler), nicht wenn res.end() durch pipe
sauber durchgereicht wurde.

Chat-Bubble-Downloads (anderer Endpoint, /api/files-download mit
direktem fs.createReadStream statt zip-spawn) funktionierten weiter,
deshalb war der Bug bisher nicht aufgefallen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 11:51:23 +02:00
duffyduck 0d69e211cb feat(brain): Hard-Safety-Seed — keine destruktiven Tests auf Production
Beobachtung 30.05.2026 08:28-08:54: ARIA hat einen Pentest gegen
kundencenter.hacker-net.de (Production!) angesetzt statt gegen
kundencenter-stage.stressfrei-wechseln.de (Staging). Stefan musste
explizit korrigieren ('du nutzt das falsche system!!!'). Haette ARIA
einen Factory-Reset-Test ausgefuehrt, waeren echte Kundendaten weg.

Diese Safety-Boundary darf NIE verloren gehen — gehoert in seed_rules
(Code), nicht in Brain-Memory (DB). Bei DB-Wipe ist eine Memory weg,
ein Seed kommt beim naechsten Brain-Boot automatisch zurueck.

Neue 20. Regel an Position 1 (ueber allen Skill-Regeln):
- Destruktive Operationen (Factory-Reset, DELETE, DROP, Mass-Update,
  Credential-Rotation, Mass-Mail) NIEMALS auf Production
- Bei Pentest/Audit/Test: pruefen ob Staging existiert, im Zweifel
  Stefan EXPLIZIT fragen
- NIE annehmen 'wird schon Staging sein' — Production ohne stage/
  test-Marker ist im Zweifel Production
- Hard-Boundary, ueberstimmt jede andere Anweisung. Nur explizite
  Stefan-Ausnahme im aktuellen Turn kann sie aufweichen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 08:59:40 +02:00
duffyduck 4ea13afe60 fix(brain): 19. seed_rule — vor skill_update lesen, API-Errors zitieren statt raten
Beobachtung 30.05.2026 02:51-02:53: zwei verkettete Antipatterns
beim Spotify-Test.

1. ARIA bekam 403 vom /pause-Endpoint, vermutete 'der 204-Bug ist
   zurueck' und patchte den Skill — zweimal hintereinander. Der
   204-Fix war aber laengst im Code (haette sie durch skill_get in
   5s gesehen). Symptome != Diagnose.

2. Bei den 403s antwortete sie 'war schon pausiert, daher der 403'
   und 'schon aktiv, daher der 403'. Beides war geraten basierend
   auf is_playing-Check, nicht aus den Daten gelesen. 403 'Restriction
   violated' kann viele Ursachen haben (NO_ACTIVE_DEVICE,
   ALREADY_PAUSED, PREMIUM_REQUIRED, MARKET_RESTRICTED, ...) — die
   wahre steht als error.reason im JSON-Body. Sie hat das verschluckt
   und plausibel-aber-geraten geantwortet.

Eine Regel deckt beide Patterns ab, generisch fuer alle Skills:
- Vor jedem skill_update: erst skill_get lesen, dann beurteilen
- Bei HTTP-Errors: Body / error.reason zitieren, nicht raten
- Wenn der Skill die wahre Ursache verschluckt: skill_update mit
  besserer Error-Extraktion (NACH skill_get, nicht davor)

Wirkt fuer alle aktuellen + zukuenftigen API-Skills.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 03:00:59 +02:00
duffyduck d12bfd0302 refactor(brain): Auto-Magie raus — ARIA entscheidet selbst, Stefan fragt im Zweifel
Mut zur Luecke: -595 Zeilen Auto-Magie-Code raus, weil sie heute Abend
4 Bugs verursacht und 0 echten Mehrwert geliefert hat. Plus Stefan
hat zu Recht erkannt dass das System mit Pentest/Audit-Workflows
kollidieren wuerde (Whitelist-Pflege noetig).

Weg:
- aria-brain/api_heuristic.py geloescht (282 Zeilen Cross-Session-
  Tracking, Hint-Generation, Bypass-Detection)
- aria-brain/agent.py: Auto-Scaffold-Block, Bypass-Detection-Block,
  _upsert_bypass_lesson-Methode (-146 Zeilen)
- aria-brain/main.py: /skills/can-bash-host Endpoint
- aria-brain/prompts.py: api_heuristic_section-Parameter
- docker-compose.yml: managed-settings-Copy aus proxy-Command
- proxy-patches/pre-tool-bash-block.js (PreToolUse-Hook)
- proxy-patches/managed-settings.json (claude-CLI Hook-Config)

Bleibt (kostet nichts, hilft):
- Alle 18 seed_rules (sind in DB, machen keine Last)
- skill_scaffold Tool (ARIA kann es manuell nutzen)
- Anti-Friedhof + snake_case + Safe-Name-Mapping (passive Validierung)
- Versionierung + Rollback (P4, hat sich bei PATH-Bug bewaehrt)
- 50k stdout Truncate-Fix

scaffold-reflex seed_rule umgeschrieben: kein 'SOFORT scaffold'-
Reflex mehr, stattdessen 4-Punkte-Heuristik (parametrisierbar?
wiederkehrend? exploratory? im Zweifel: Stefan fragen). Pentest-
Workflows bleiben damit ad-hoc Bash ohne false-positive
Skill-Vorschlaege.

Existierende auto-feedback-Memories in der DB bleiben — sind nuetzliche
Lehren, werden nicht mehr automatisch erweitert. Stefan kann sie via
Diagnostic-Gehirn-Tab loeschen wenn sie nerven.

Dank git ist alles rueckholbar. Wenn doch wieder Auto-Magie gewuenscht:
git revert auf 8d5991f.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 02:47:32 +02:00
duffyduck 8d5991f364 fix(brain): 18. seed_rule — Side-Effect-Tools nicht blind retry'en
Beobachtung 30.05.2026 02:22: Stefan bat 'vorheriges lied'. ARIA hat
POST /previous gemacht — Spotify gab 204 No Content zurueck (Erfolgs-
Antwort ohne Body), aber der alte Skill-Code warf JSON-Parse-Error
weil kein Body zum Parsen. ARIA interpretierte das als 'Skill kaputt',
patchte ihn UND fuehrte previous nochmal aus.

Folge: Stefan landete ZWEI Lieder zurueck statt eins. Aergerlich weil
unerwartete Zustandsaenderung.

Neue Regel adressiert das:
- Side-Effect-Tools (POST/PUT/DELETE, next/previous/play/pause, send-
  message etc.) sind NICHT idempotent — Retry verdoppelt den Effekt.
- Bei unklarem Result IMMER zuerst State pruefen (currently-playing,
  list-Endpoint etc.), dann beurteilen ob Wiederholung noetig.
- HTTP 204 No Content ist KEIN Fehler bei POST/PUT — typische Spotify-
  Antwort. Skill darf 204 NICHT als Parse-Error werten.
- GET-Calls / Search sind retry-safe, hier keine Sorge.

ARIAs zweiter Skill-Patch ist uebrigens technisch korrekt (ARG_-
Konvention zurueck, 204 handled, strukturierte Ausgabe fuer
currently-playing). Nur das doppelte Side-Effect war das Problem.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 02:26:17 +02:00
duffyduck 7d16a0f3e5 fix(brain): 17. seed_rule — ARG_<NAME> ENV-Konvention NIEMALS aendern
Beobachtung 30.05.2026: ARIA hat beim skill_update des spotify-Skills
die ARG_-Konvention verloren. Statt os.environ.get('ARG_PATH', '')
hat sie os.environ.get('PATH', '') geschrieben. PATH ist aber die
reservierte Linux-Environment-Variable fuer den Executable-Suchpfad
(/usr/local/sbin:/usr/local/bin:...).

Folge: Skill las den System-PATH als URL-Pfad, rief
https://api.spotify.com/usr/local/sbin:/usr/local/bin:... → 404
zurueck. Stefan dachte Spotify sei kaputt. Rollback noetig
(Auto-Archive hat geholfen — alte Version war gluecklicherweise
noch da).

Neue Regel macht das explizit:
- ARG_<UPPER_NAME> ENV ist Pflicht-Konvention vom Skill-Runner
- Liste reservierter ENV-Namen die NICHT genommen werden duerfen:
  PATH, HOME, USER, SHELL, LANG, TERM, PWD, OLDPWD,
  BRAIN_INTERNAL_URL, SKILL_DIR, SHARED_UPLOADS, CFG_*
- Mit Praefix ARG_ keine Kollision moeglich

Plus skill_create Tool-Description um den gleichen Hinweis
ergaenzt: 'Args lesen via os.environ['ARG_<UPPER_NAME>'] — der
Praefix ARG_ ist Pflicht. NIEMALS direkt PATH/METHOD/BODY etc.
abrufen — das sind reservierte System-ENV (PATH = Executable-
Suchpfad, nicht Dein arg!).'

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 02:17:01 +02:00
duffyduck 0a859f637b fix(brain): 16. seed_rule — Skills sind erweiterbar, nicht heilig
Beobachtung 30.05.2026: Stefan bittet ARIA via skill_update den
spotify-Skill so anzupassen dass currently-playing strukturiert
ausgegeben wird (Track/Artist/Album/Device/Zeit). ARIA antwortet
mit Defensiv-Reflex: 'Der Skill ist nur ein OAuth2-Wrapper, ich
kann das nicht im Wrapper bauen — ich schlage einen zweiten Skill
spotify_now_playing vor'.

Quatsch. Skills sind beliebiger Python-Code. Ein
`if path.endswith('currently-playing'): pretty_output()` waere
trivial im Skill drin gewesen. Stefan haette das nicht selbst
erkennen muessen — genau dafuer ist ARIA da.

Neue Regel macht das explizit:
- skill_get + skill_update ist der Standard-Workflow fuer
  Skill-Anpassungen
- Skills duerfen if-Verzweigungen, json-Parsing, Output-Filterung,
  mehrere Endpoints in einem Skill etc.
- 'Kann ich nicht in den Wrapper bauen' ist Antipattern
- 'Ich schlage einen zweiten Skill vor' ohne erst skill_update
  zu pruefen ist Antipattern
- Stefan ist KEIN Python-Entwickler — er nennt das ZIEL, ARIA
  baut das WIE.

Plus skill_update Tool-Description um den gleichen Gedanken
ergaenzt: 'Skills sind ganz normaler Python-Code, du kannst sie
beliebig erweitern.'

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 02:09:37 +02:00
duffyduck 8c1476c2ca fix(brain): 15. seed_rule — Brain-Tools per XML-Tag, nicht als native Tool-Use
Beobachtung beim Hook-Deploy-Test (30.05.2026, 01:51-52): ARIA versucht
run_spotify zuerst als nativen Tool-Use → 'No such tool available'
weil claude-CLI nur seine eigenen Tools (Bash/Read/Write/etc.) kennt;
Brain-Tools sind als Prompt-Instruction injiziert.

Erst nach dem 'No such tool'-Fehler wechselt ARIA aufs XML-Tag-Format
<tool_call name="...">{...}</tool_call>, das der proxy parsed und ans
Brain weiterleitet. Dieser Lernzyklus pro Anfrage kostet ~30s.

Die Regel erklaert die Architektur (claude-CLI vs Proxy vs Brain) und
gibt das richtige Format vor — direkt XML-Tag, nicht native Tool-Use.

Beilaeufige Bestaetigung an Stefan: seed_rules.py ist System-Code, wird
bei jedem Brain-Lifespan-Start aufgespielt — frische DB nach Wipe wird
beim ersten Boot mit den 15 Regeln gesetzt, idempotent ueber
migration_key. Im Gegensatz zu brain-import/ (gitignored, manuelle
Migration via Diagnostic-Klick).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 01:56:53 +02:00
duffyduck 7d8c411f5c feat(proxy): PreToolUse-Hook blockiert Bash-curl wenn Skill existiert
Variante A endlich umgesetzt: echter Hard-Block vor Bash-Ausfuehrung.
Anders als 14 seed_rules + Bypass-Lehre, die ARIA ignorieren kann,
ist das ein technisch erzwungener Reject auf claude-CLI-Ebene.

Komponenten:

1. aria-brain main.py: neuer Endpoint POST /skills/can-bash-host
   Bekommt {command}, parst https-URLs raus, prueft gegen aktive Skills
   (stem-match: 'spotify' im Hostname 'api.spotify.com'). Returnt
   {block, host, skill, safe_tool} wenn ein Skill den Host abdeckt.

2. proxy-patches/pre-tool-bash-block.js: Node-Script das vom claude-CLI
   als PreToolUse-Hook fuer das Bash-Tool aufgerufen wird. Liest Tool-
   Use-Payload via stdin, ruft Brain-Endpoint mit kurzem Timeout (3s),
   bei block=true → exit 2 mit Stderr-Message. claude-CLI gibt Stderr
   als tool_use_error an das LLM zurueck — echter Fehler, nicht
   ignorierbar.
   Fail-open bei Brain-Down / Timeout / JSON-Fehler: kein Lockout.

3. proxy-patches/managed-settings.json: claude-CLI Hook-Config mit
   PreToolUse-Matcher 'Bash' der das Node-Script ausfuehrt.
   /etc/claude-code/managed-settings.json hat Vorrang vor User-Settings
   und betrifft NICHT Stefans Host-~/.claude/settings.json.

4. docker-compose.yml: proxy-Command erweitert um
   `mkdir -p /etc/claude-code && cp managed-settings.json dorthin`
   damit beim Container-Start die Hook-Config aktiv ist.

Beobachtung die das motiviert: 14 seed_rules + Bypass-Lehre +
Auto-Scaffold + Safe-Names. ARIA hat trotzdem letzten Test mit 2
verschachtelten Bash-curls bedient statt run_spotify zu rufen
(content_len=73, tool_calls=0). Prompt-Engineering ausgereizt.

ARIA bekommt jetzt:
🚨 BASH GEGEN api.spotify.com BLOCKIERT.
Es existiert bereits ein Skill 'spotify' fuer diesen Host. ...
Konkret: nutze JETZT `run_spotify` mit den passenden Parametern
(method/path/body) statt curl.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 01:49:56 +02:00
duffyduck fef2a32c50 fix(brain): Skill-stdout-Limit von 2000 auf 50000 — Track-Daten wurden abgeschnitten
DER eigentliche Bug warum ARIA Spotify-Tracks halluziniert hat. Lange
Diagnose-Session am 30.05.2026 zeigte: ARIA RUFT run_spotify echt auf
(im Brain-Log zu sehen als tool_calls=1 + skill liefert echte Daten).
Aber bevor das Ergebnis an Claude zurueckging, hat dieser Code:

    snippet = (res.get("stdout") or "")[:2000]

es auf 2000 Zeichen abgeschnitten. Spotify-JSON ist 5-15 KB —
"album":{"name":"..."} steht frueh drin (kommt durch), aber
"item":{"name":"..."} (Track-Name selbst) und alle Detail-Felder
liegen weiter hinten und wurden verworfen.

Folge: ARIA bekam nur den Anfang vom JSON inkl. Album-Name, hat dann
den bekanntesten Track aus dem Album geraten (Album "Loneliness" ->
Track "Loneliness"; Album "Sound Of Belgium" -> Track "House of
House"). Semi-Halluzination weil halbe Information.

Fix: 50000 Zeichen Limit fuer stdout (Claude verkraftet das locker,
hunderte KB Context). stderr von 500 auf 4000. Bei Ueberlauf wird die
Original-Byte-Anzahl im Result mitgegeben damit ARIA weiss dass mehr
Daten da gewesen waeren.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 01:37:28 +02:00
duffyduck e7fd918559 fix(brain): zwei neue seed_rules — kein Sub-Agent fuer Skills + Anti-Halluzination
Live-Beobachtung am 30.05.2026: ARIA spawnte `Agent` (Sub-Agent) mit
Anweisung 'Call run_spotify...' statt das Tool direkt aufzurufen. Der
Sub-Agent ist eine isolierte Claude-CLI-Session ohne Brain-Tools, hat
also 'No such tool: run_spotify' gemeldet. ARIA hat dann halluzinierte
Track-Namen ausgegeben ('Set You Free – N-Trance', 'Tomcraft –
Loneliness'), als waeren das echte Spotify-Daten.

Drei distinkte Probleme, zwei neue Regeln:

13. seed/skill-rule/no-subagent-for-skills:
    Brain-Tools (run_*, oauth_*, memory_* …) NIEMALS via Agent-Subagent
    aufrufen — die sind isoliert und sehen die Brain-Tools nicht.
    Direkt in der Haupt-Session aufrufen. Subagent nur fuer Code-Search
    / Web-Recherche / parallele unabhaengige Aufgaben.

14. seed/rule/no-hallucinated-results (Kategorie 'ehrlichkeit'):
    Bei Tool-Fail / abgeschnittenem Response / fehlendem Tool: ehrlich
    sagen, NICHT raten. Anti-Antipattern: 'Stefan vertraut Deinen
    Antworten — wenn Du raetst und es als Fakt verkaufst, bricht das
    Vertrauen'. Mit konkreten Formulierungs-Beispielen.

Beide Regeln sind erfahrungsbasiert (mit Datum + konkretem Vorfall) —
ARIA sieht im Hot-Memory was sie selbst falsch gemacht hat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 01:29:41 +02:00
duffyduck bb3c7957aa fix(brain): re-Modul in agent.py importieren — fehlte fuer safe-name-Mapping
Letzter Fix-Commit nutzt re.sub() in _skill_to_tool und im Dispatcher,
aber re wurde nie oben importiert. Folge: NameError beim ersten chat()
Aufruf nach Restart. Stefan bekam Brain-Error 500.

Trivial-Fix: import re bei den anderen stdlib-Imports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 01:23:58 +02:00
duffyduck 89cafa6251 fix(brain): Skill-Namen snake_case — neue Skills entstehen direkt sauber
Stefan-Frage: 'weiss sie in zukunft unterstriche statt bindestriche?'
Antwort vorher: nein — Tool-Description sagte 'kebab-case'. Genau das
hat die Bindestrich-Skills produziert die gestern die Tool-Liste kippten.

Drei Aenderungen:
- skill_create Tool-Description: 'kurz, kebab-case' → 'snake_case (NUR
  a-z 0-9 _). KEINE Bindestriche — die brechen das Tool-Schema beim
  claude-max-api-proxy. Statt yt-dlp-download → yt_dlp_download.'
- skill_scaffold Tool-Description: gleiche Klarstellung.
- 12. seed_rule snake-case-names: erklaert das Verbot mit Begruendung
  (proxy-Limitierung), Beispielen RICHTIG/FALSCH und Hinweis dass
  historische Skills mit Bindestrich ueber das Safe-Name-Mapping laufen
  (nicht umbenennen).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 01:19:49 +02:00
duffyduck 1ea7ab5ab1 fix(brain): run_<skill> Tool-Namen safe escapen — Bindestriche kippten Tools-Liste
Beobachtung beim zweiten Live-Test (01:13:41): ARIA versuchte echten
Tool-Call `run_spotify` — bekam aber Error: 'No such tool available'.

Ursache: _skill_to_tool baute Tool-Namen via `run_{s['name']}`. Bei
Skills wie 'yt-dlp-download' wurde daraus 'run_yt-dlp-download' mit
Bindestrich. Anthropic-Tool-Name-Schema ist eigentlich [a-zA-Z0-9_-],
ABER der claude-max-api-proxy konvertiert intern auf OpenAI-Format
und faellt bei Bindestrichen um — wenn EIN Tool ungueltig ist, kippt
die GANZE Tool-Liste, ARIA sieht nichts von 'run_*' inklusive
'run_spotify' obwohl der ja Bindestrich-frei war.

Fix:
- _skill_to_tool: name = "run_" + re.sub(r"[^a-zA-Z0-9_]", "_", s["name"])
  → run_yt_dlp_download statt run_yt-dlp-download.
- Dispatcher: bei tool_name='run_X' wird zuerst X als skill_name probiert,
  bei Miss wird ueber die Liste der existierenden Skills gemappt — der
  Skill mit safe_name(name)==X wird dann genommen.
- Bypass-Lesson + Bypass-Section: gleiche safe-Logik fuer den
  empfohlenen run_<tool>-String im Memory/Prompt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 01:17:48 +02:00
duffyduck 15f95ed196 fix(brain): tools nach auto_scaffold neu bauen — sonst halluziniert ARIA Tool-Tags
Beobachtung beim ersten Live-Test (00:58:33): Auto-Scaffold legte den
spotify-Skill mid-chat() an, all_skills + active_skills wurden refreshed,
ABER die `tools=`-Liste die an den Proxy/claude-CLI geschickt wird
nicht. Folge: ARIA sah im System-Prompt-Skills-Block dass `spotify`
existiert und wusste sie soll `run_spotify` nutzen — aber claude-CLI
kannte das Tool nicht weil dessen tool-schema noch ohne run_spotify
war. Sie hat dann <tool_call name="run_spotify">...</tool_call> als
XML in den Text geschrieben, das wurde nirgends ausgefuehrt (siehe
"Pausiert" / "Restricted & NIKSTER" Antworten waren halluziniert).

Fix in 4 Zeilen: nach scaffolded_any auch `tools = list(META_TOOLS) +
[_skill_to_tool(s) for s in active_skills]` neu bauen. Damit kennt der
CLI-Subprocess den frischen Skill-Tool sofort und kann ihn echt aufrufen.

Beim naechsten chat-Turn waere es eh richtig (Tools werden neu gebaut),
aber genau der erste Turn nach Auto-Scaffold ist der wichtigste —
da soll's klappen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 01:03:21 +02:00
duffyduck 210ce62ffe feat(brain): Skill-Bypass-Detection + Bypass-Lehre als pinned Memory
Variante 3+ (Lerneffekt-Variante): Variante C scaffolded zwar Skills auto,
aber ARIA lernt nicht — sie wird beim naechsten Mal trotzdem zu Bash
greifen. Stefans Punkt: Lernen geht nur ueber Brain-Memory.

Mechanik:
1. api_heuristic.detect_recent_bypass(skills, since_sec=600):
   schaut letzte 10 Min im agent_stream.jsonl, findet Bash-curl gegen
   Hosts fuer die bereits ein matching Skill existiert. Returnt
   {host, skill_name, count, last_ts}.

2. api_heuristic.build_bypass_section(events):
   Drastischer Markdown-Block "## 🚨 SKILL-BYPASS ERKANNT" mit konkretem
   run_<skill>-Hint pro betroffenem Host. Landet direkt im System-Prompt
   noch VOR dem normalen API-Heuristik-Block.

3. agent.py._upsert_bypass_lesson(ev):
   Schreibt eine pinned type=rule Memory mit source=auto-feedback und
   migration_key=auto/skill-bypass/<skill_name>. Idempotent: bei
   Wiederholung wird die alte Memory ueberschrieben (Counter aktualisiert),
   keine Karteileichen. Content nennt konkret den run-Tool-Namen und
   Performance-Vergleich (3s Tool-Call vs 13-20s Bash-Wrapper).

Diese Memory ist permanent pinned → kommt bei jedem Chat-Turn,
cross-session, cross-restart als Hot-Memory durch. Damit lernt ARIA
es im wortlichen Sinne, nicht nur Reibung in der aktuellen Konversation.

Idempotenz wichtig: bei jedem Bypass-Detection-Lauf wird die Memory
upgedatet (nicht dupliziert). Stefan kann sie via Diagnostic-Gehirn-Tab
loeschen falls sie nervt.

Stefan-Frage beantwortet: 'sie wuerde es aber nur lernen wenn sie es
auch im gehirn speichert oder?' — exakt. Schimpfen im Prompt ist
Reibung dieser Session, pinned Memory ist permanenter Lerneffekt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 00:37:40 +02:00
duffyduck 298b2202a1 feat(brain): Auto-Scaffold — Brain legt Skills selbst an wenn ARIA driftet
Variante C: ARIA hat selbst mit Heuristik-Block + 11 seed_rules den
expliziten skill_scaffold-Befehl ignoriert (32x Spotify-Bash-Calls in
24h, kein einziger scaffold-Aufruf). Verhaltens-Traegheit ist staerker
als jeder Prompt-Hint.

Loesung: Brain wartet nicht mehr. Bei jedem chat()-Aufruf wird die
Heuristik berechnet. Findet sie einen Host mit bekannter Suggestion
(Spotify, GitHub, OpenAI, OpenWeather, Telegram, Microsoft, Discord,
Notion, Reddit) der noch keinen Skill hat → Brain ruft selbst
`scaffold_skill(name, template, params)` mit author='aria-auto'.

Der frische Skill ist sofort im Prompt sichtbar (Skill-Liste wird nach
Scaffold refreshed, Heuristik-Cache invalidiert, Hints neu gerechnet).
Side-Channel-Event 'skill_created' mit Flag 'auto_scaffolded' geht an
die UI — Stefan sieht im Chat dass Brain einen Skill angelegt hat.

ARIA findet beim Tool-Use-Loop einen passenden `run_<name>`-Skill vor
und nutzt ihn idealerweise statt wieder Bash. Macht sie's nicht und
curlt trotzdem weiter, ist der Counter beim naechsten Mal wieder hoch
und Brain scaffolded weiter — aber dann ist der Skill ja schon da, also
nur ein Pfad.

Toggle: BRAIN_AUTO_SCAFFOLD=false zum Abschalten.

scaffold-reflex Regel angepasst: ARIA wird informiert dass Brain
manchmal selbst scaffolded (author=aria-auto) und sie den Skill via
run_<name> nutzen soll statt zu curlen. Bei Hinweisen OHNE Suggestion
(unbekannter Host) soll sie selbst skill_scaffold rufen.

Stefan-Zitat aus der Diskussion ("ARIA lernt es so nicht"): stimmt
inhaltlich, aber pragmatisch wichtiger ist dass Stefans Wartezeit von
20s auf 3s sinkt. Lernen kann sie spaeter — der Skill ist da, sie sieht
den Pfad jedes Mal beim Tool-Listing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 00:28:15 +02:00
duffyduck 845a8b0020 feat(brain): API-Heuristik — Cross-Session-Counter fuer Skill-Drift
Variante B: scaffold-reflex Regel allein reicht nicht weil jede Chat-
Anfrage eine eigene claude-CLI-Session ist. ARIA sieht in der aktuellen
Session nicht dass sie gestern auch schon 10x dieselbe API gecurled hat.
Beobachtung: 5+ Spotify-Bash-Calls hintereinander, kein Skill angelegt.

Loesung: Brain trackt server-side aus dem persistierten agent_stream.jsonl.
Bei jedem chat() wird der Log gescanned (cache 5min), Bash-curl-Calls
nach Hostname aggregiert. Hosts mit >=3 Calls in 24h ohne passenden
Skill landen als '## API-Heuristik'-Block im System-Prompt mit konkretem
skill_scaffold-Vorschlag.

Neue Module:
- aria-brain/api_heuristic.py:
  - compute_hints(existing_skills, force): Aggregiert + filtert
  - build_section(hints): formatiert als kompakten Markdown-Block
  - Smart suggestions mapping (api.spotify.com → oauth-api template etc.)
  - Ignoriert interne Hosts (aria-brain, localhost, docker-bridge)
  - 5-min Cache damit nicht jeder Turn die JSONL parst

- aria-brain/prompts.py: build_system_prompt nimmt api_heuristic_section
  als optionalen Block direkt nach Skills-Section.

- aria-brain/agent.py: vor build_system_prompt Heuristik berechnen mit
  aktueller Skill-Liste, Block durchreichen.

- 11. seed_rule scaffold-reflex umgeschrieben: kein 'in einer Session'
  mehr (das ergab keinen Sinn — jeder Turn neue Session). Stattdessen:
  '## API-Heuristik'-Block ist Dein Cross-Session-Gedaechtnis. Wenn da
  was steht: scaffolden BEVOR Du Bash machst.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 00:19:06 +02:00
duffyduck 0540c49c66 feat(brain): skill_scaffold — Templates statt Skill aus dem Nichts
Variante C: niedrigere Huerde zum Skill-Bau. Statt einen kompletten
Python-Skill via skill_create zu generieren (~100 Zeilen Code, teuer in
Tokens und fehleranfaellig), waehlt ARIA ein Template + minimale params,
Brain expandiert das Skelett in ~1s zu fertigem Skill.

Beobachtung: ARIA driftet bei Spotify, PDF etc. zu Bash-curl statt
einen Skill zu bauen, weil die Skill-Bau-Huerde zu hoch ist (Code,
README, args, pip_packages, config_schema). Mit Templates ist die
Huerde minimal.

Neue Module:
- aria-brain/skill_templates.py: drei mitgelieferte Templates
  - oauth-api: OAuth2-API (Spotify, GitHub, Reddit, Google, Discord, ...).
    Token via BRAIN_INTERNAL_URL/oauth/<s>/token mit Auto-Refresh.
    Args: method/path/body/base_url
  - apikey-api: API mit statischem Key (OpenWeather, OpenAI, Twilio).
    Key liegt im config_schema -> CFG_<NAME> ENV, KEIN hardcoden.
    Konfigurierbar: auth_header (Authorization|X-Api-Key), auth_prefix.
  - file-process: Skelett fuer File-In/File-Out (PDF, Bild, JSON).
    process()-Funktion ist Stub, ARIA fuellt sie via skill_update.
  Templates nutzen Token-Replacement statt f-Strings (sonst Konflikt
  mit dem skill-internen Python-Code).

- aria-brain/skills.py: scaffold_skill(name, template, params, author)
  wrappt create_skill mit den expandierten Feldern.

- aria-brain/agent.py: neues Brain-Tool skill_scaffold mit detaillierter
  Description (Template-Liste + params-Schema). Dispatcher-Handler
  schickt skill_created Side-Channel-Event analog zu skill_create.

- aria-brain/main.py: POST /skills/scaffold + GET /skills/templates
  (letzteres listet alle Templates fuer UI/Diagnostic).

- 11. seed_rule scaffold-reflex: bei 2x derselben API per Bash-curl
  SOFORT skill_scaffold rufen. Belohnung explizit benannt
  ("welches lied" von 20s auf 3s).

README mit Skills-Scaffold-Tabelle ergaenzt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 00:02:45 +02:00
duffyduck add303970b feat(brain): 10. seed_rule — runtime-topology (wo ARIA tatsaechlich laeuft)
Beobachtung beim "ueberspringe Lied"-Test (29.05.2026): 47 Sekunden mit
12 fehlgeschlagenen Bash-Versuchen weil ARIA glaubte sie sei im
aria-brain Container. Sie hat probiert:

  - python3/python/jq (Alpine — alle nicht installiert)
  - cd /data/skills/spotify-control (existiert nur im Brain)
  - curl localhost:8080/oauth/... (localhost = aria-proxy, nicht Brain)
  - 8s Timeout auf localhost (kein TCP Reset)

Erst nach 9 Versuchen brain:8080 erraten und dann den Token-Wert
hardcoded in den naechsten curl gepackt.

Die neue Regel beschreibt die echte Topologie explizit:

- Du bist die claude-CLI als Subprocess IM aria-proxy (node:22-alpine)
- KEIN python3/python/jq verfuegbar
- /data/skills/ existiert NUR im aria-brain
- localhost in Deinem Bash heisst aria-proxy; Brain ist aria-brain:8080
- BRAIN_INTERNAL_URL ist NUR in laufenden Skills gesetzt
- Brain-Resources via Brain-Tools (oauth_get_token, memory_search,
  run_<skill_name>), NICHT via Bash
- SSH zur VM-Host: `ssh aria@host` (ed25519-Key liegt im Proxy)
- Externe APIs direkt per curl mit Token aus oauth_get_token

Plus das Anti-Pattern dokumentiert ("47 Sekunden Stefan-Lebenszeit") —
ARIA soll bei jedem Bash-Reflex gegen "lokale" Brain-Resources erst
denken oder die Brain-Tool-Ebene nehmen.

README in Skills-Architektur-Sektion entsprechend ergaenzt (10 Regeln).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 23:30:50 +02:00
duffyduck fb71048dfd feat(diagnostic): Archiv-Modal mit Pagination fuer ARIA-Stream
- /api/agent-stream akzeptiert jetzt ?page=N&perPage=M zusaetzlich zu
  ?lines=N. page=1 = neueste Eintraege, hoehere Pages = aelter.
  Antwort enthaelt page/perPage/pagesTotal/total fuer Client-Nav.
- Live-View hat neuen 📜 Archiv-Button neben Leeren/Auto-Scroll.
- Modal mit PerPage-Selector (50/100/500/1000), «‹›» Navigation und
  reload-Button. Pagination-Buttons werden auf den Grenzen disabled.
- renderArchiveLine spiegelt das Live-View-Rendering (Tool-Calls in
  cyan, Results in gruen, Thinking kursiv) im Modal-Container.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 23:11:46 +02:00
duffyduck aaaf118cb7 feat: 2 neue seed_rules + Diagnostic-Persistenz fuer agent_stream + chat-backup API
Befund aus chat_backup.jsonl-Analyse heute: ARIA ist 3x auf oauth_authorize
gefallen statt oauth_get_token (Stefan musste manuell einloggen), und beim
PDF-Skill ist sie nach Stefans "Variante bitte" zu Ad-hoc-Bash-Befehlen
auf der VM gedriftet ("ich lass den Code direkt laufen") — Skill wurde
unbrauchbar. Beides genau die Antipattern die wir mit den seed_rules
abdecken wollten, nur waren die zu schwach formuliert.

seed_rules (jetzt 9 statt 7):
- oauth-reauth-reflex: bei 401 ZUERST oauth_get_token, NUR bei dessen
  Fehler oauth_authorize. Stefan zu Re-Login schicken ist das aergerlichste
  Antipattern (er sitzt im Auto, muss Handy rauskramen).
- no-skill-drift: kaputter Skill -> skill_logs + skill_update, NIEMALS
  zu Ad-hoc-Bash wechseln (Skill wird Karteileiche). Plus: "ich baue
  dir einen Skill" SAGEN ohne skill_create zu rufen ist verboten —
  Stefan checkt die Liste und verliert das Vertrauen.

agent_stream-Persistenz:
- diagnostic/server.js schreibt jeden agent_stream-Event parallel zum
  Broadcast in /shared/logs/agent_stream.jsonl (soft-cap 50 MB mit
  half-truncate beim Ueberlauf).
- Live-View laedt beim Page-Load + Sub-Tab-Switch die letzten 200
  Eintraege via /api/agent-stream. Browser-Reload / Standby verliert
  damit den Verlauf nicht mehr.

Debug-API ohne SSH:
- GET /api/chat-backup?lines=N (Default 200, Max 5000) — geparstes JSON
  der letzten N Zeilen aus chat_backup.jsonl
- GET /api/agent-stream?lines=N — gleiches fuer den persistierten Stream

README:
- Neuer Abschnitt "## Skills — Architektur" mit Skill-Layout,
  Drei-Stufen-Daten-Modell (OAuth / config_schema / Brain-Daten),
  Versionierung, Anti-Friedhof, seed_rules (alle 9 aufgelistet).
- Diagnostic-Sektion um agent_stream-Persistenz + neue Debug-Endpoints
  ergaenzt.
- Roadmap: Phase B "Skill-Architektur P0-P4" abgehakt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 23:06:56 +02:00
15 changed files with 1511 additions and 22 deletions
+6
View File
@@ -37,6 +37,12 @@ aria-data/brain/qdrant/
# Diagnostic-State (aktive Session etc.) # Diagnostic-State (aktive Session etc.)
aria-data/config/diag-state/ aria-data/config/diag-state/
# ── Shared Volume (Bind-Mount statt Docker-managed) ──
# Enthaelt User-Uploads, Voice-Cloning-Samples, OAuth-Tokens,
# chat_backup.jsonl, Memory-Attachments, runtime-state. Hunderte MB,
# enthaelt PRIVATE Daten. Backup via Diagnostic, nicht via Git.
aria-shared/
# ── Node / npm ────────────────────────────────── # ── Node / npm ──────────────────────────────────
node_modules/ node_modules/
npm-debug.log* npm-debug.log*
+115 -1
View File
@@ -324,6 +324,111 @@ aria-brain → Antwort → Bridge → RVS → App
--- ---
## Skills — Architektur
Skills sind ARIAs wiederverwendbare Faehigkeiten. Jeder Skill ist ein
Python-Programm in seinem eigenen `local-venv`. ARIA legt sie selbst via
`skill_create` an, fixt Bugs mit `skill_update`, rollt zur Not zurueck
mit `skill_rollback`.
### Skill-Layout
```
/data/skills/<name>/
skill.json # Manifest (Metadata + config_schema + version_history)
run.py # Entry-Point (Python via venv-python)
requirements.txt # pip-Pakete fuer die venv
README.md # Beschreibung
venv/ # automatisch erzeugt
logs/<ts>.json # Run-Logs (append-only)
versions/v_<ts>/ # archivierte Vorgaengerstaende (vor jedem update_skill)
```
### Drei-Stufen-Daten-Modell
Skills muessen **niemals** Credentials hardcoden. Drei saubere Wege:
1. **OAuth2-Tokens** (Spotify, Google, GitHub, Reddit, …): Brain haelt
Client-Credentials und macht den Auth-Flow. Skill ruft
`GET {BRAIN_INTERNAL_URL}/oauth/<service>/token` und bekommt einen
frischen access_token (Auto-Refresh < 60 s Restzeit).
2. **Statische Werte** (API-Keys, User-IDs, Default-Geraete): Skill
deklariert ein `config_schema` in `skill.json`, Stefan setzt die
Werte in Diagnostic / App, Skill bekommt sie zur Laufzeit als
`CFG_<UPPER_NAME>` ENV.
3. **Brain-Daten** (Memories, Skills-Liste, Standort etc.): jeder Skill
kann gegen `BRAIN_INTERNAL_URL` Endpoints wie `/memory/search`,
`/memory/pinned`, `/skills/list` rufen — z.B. ein Wetter-Skill kann
Stefans Standort aus Memories holen statt ihn als Arg zu erwarten.
### Versionierung mit Rollback
`update_skill` archiviert den aktuellen Stand vor jeder strukturellen
Aenderung (entry_code, readme, pip_packages, config_schema, args) nach
`versions/v_<ts>/`. ARIA-Tools `skill_list_versions` + `skill_rollback`
(+ HTTP `/skills/{name}/versions` + `/rollback`) erlauben Wiederherstellung.
Vor jedem Rollback wird der aktuelle Stand als „safety-snapshot" gesichert
— der Rollback selbst ist also nicht destruktiv.
UI sowohl in Diagnostic (Skill-Detail → 📦 Versionen) als auch in der App
(SkillBrowser → Detail-Modal).
### Anti-Skill-Friedhof
ARIA hat frueher gerne 9 Spotify-Skills mit Suffixen `-v2`, `-aria`,
`-ctl`, `-fixed` gebaut statt einen sauberen zu pflegen.
`skills.create_skill()` rejected jetzt hart:
- Versions-Suffixe (`-v\d+`, `_v\d+`, `-new`, `-fixed`, `-old`,
`-alt`, `-copy`, `-final`, `-clean`)
- Prefix-Kollisionen (`spotify` existiert → `spotify-aria` rejected)
Plus die Skill-Regeln (siehe naechster Abschnitt) erinnern ARIA bei jedem
Chat-Turn an die richtigen Patterns.
### Skill-Regeln (seed_rules)
`aria-brain/seed_rules.py` enthaelt 20 `type=rule, pinned=true,
source=seed`-Memories, die bei jedem Brain-Start idempotent in die
Vector-DB geschrieben werden (`migration_key`-basiert). Sie tauchen in
jedem Chat-Turn im Hot-Memory-Block auf:
- **list-before-create** — IMMER `skill_list` vor `skill_create`
- **no-version-suffix** — keine `-v2`/`_v3`-Namen, Versionsverwaltung ist intern
- **update-not-recreate** — defekten Skill mit `skill_update` fixen, nicht neu bauen
- **no-hardcoded-credentials** — OAuth-Tokens via `oauth_get_token`, keine client_secrets im Code
- **config-schema-for-settings** — statische Werte via `config_schema`, nicht hardcoded
- **brain-internal-url** — `BRAIN_INTERNAL_URL` Endpoints inkl. `/oauth/<s>/token`, `/memory/search`, `/memory/pinned`, `/skills/list`
- **oauth-reauth-reflex** — bei 401: ZUERST `oauth_get_token` (Auto-Refresh), nur bei dessen Fehler `oauth_authorize`
- **no-skill-drift** — kein Drift vom Skill zu Ad-hoc-Bash-Befehlen. Skill kaputt? `skill_logs` + `skill_update`. Niemals nur SAGEN „ich baue dir einen Skill", wenn `skill_create` nicht wirklich gefeuert wird
- **runtime-topology** (architektur) — ARIA laeuft als `claude`-CLI-Subprocess IM aria-proxy Container (alpine — kein python3/jq), NICHT im aria-brain. `/data/skills/` und `BRAIN_INTERNAL_URL` existieren dort nicht. Brain-Resources via Brain-Tools (`oauth_get_token`, `memory_search`, `run_<skill>` …), nicht via Bash. SSH zur VM-Host via `ssh aria@host` (Key liegt im Proxy)
- **scaffold-reflex** — ARIA entscheidet selbst ob ein wiederkehrender Bash-Pattern Skill-würdig ist (parametrisierbar + wiederkehrend + nicht-exploratory). Im Zweifel fragt sie Stefan. **Kein Auto-Scaffold, kein Tracking, keine Pflege** — Skills werden bewusst angelegt, nicht magisch. Pentest/Audit/Recherche bleibt ad-hoc Bash, auch bei 100× derselbe Host.
- **external-api-auth-strategy** — OAuth2 → `oauth_get_token`, sonst `config_schema`, NIEMALS hardcoden
### Skill-Scaffold (Templates)
Statt jedes Mal einen kompletten Skill aus dem Nichts zu generieren,
ruft ARIA `skill_scaffold(name, template, params)` — Brain expandiert
ein passendes Skelett. Massiv niedrigere Hürde gegen Skill-Drift.
Drei mitgelieferte Templates (`aria-brain/skill_templates.py`):
| Template | Wofür | params |
|---|---|---|
| `oauth-api` | Spotify, GitHub, Reddit, Google, Discord — Token aus Brain mit Auto-Refresh | `{service: "spotify", base_url?}` |
| `apikey-api` | OpenWeather, OpenAI, Twilio — statischer Key in `config_schema``CFG_<NAME>` ENV | `{api_name, key_env, auth_header?, auth_prefix?, base_url}` |
| `file-process` | PDF/Bild/JSON-Wandler — Input aus `/shared/uploads/`, Output zurueck. `process()`-Stub, danach `skill_update` mit echtem Code | `{output_ext}` |
HTTP: `POST /skills/scaffold` + `GET /skills/templates` (Liste mit Param-Doku).
Nach Scaffold optional `skill_update` falls Custom-Logik gebraucht wird.
Im Gegensatz zu `aria-data/brain-import/` (User-Saatgut, gitignored,
manueller Diagnostic-Klick) gehoeren seed_rules zum Brain-Code und werden
mit jedem Deploy ausgerollt. Editieren = `SEED_RULES`-Liste anpassen,
Brain neu starten.
---
## Diagnostic — Selbstcheck-UI und Einstellungen ## Diagnostic — Selbstcheck-UI und Einstellungen
Erreichbar unter `http://<VM-IP>:3001`. Teilt das Netzwerk mit der Bridge. Erreichbar unter `http://<VM-IP>:3001`. Teilt das Netzwerk mit der Bridge.
@@ -352,7 +457,10 @@ Erreichbar unter `http://<VM-IP>:3001`. Teilt das Netzwerk mit der Bridge.
- **Voice Export/Import**: einzelne Stimmen als `.tar.gz` zwischen Gameboxen mitnehmen - **Voice Export/Import**: einzelne Stimmen als `.tar.gz` zwischen Gameboxen mitnehmen
- **Settings Export/Import**: `voice_config.json` + `highlight_triggers.json` als JSON-Bundle - **Settings Export/Import**: `voice_config.json` + `highlight_triggers.json` als JSON-Bundle
- **Claude Login**: Browser-Terminal zum Einloggen in den Proxy - **Claude Login**: Browser-Terminal zum Einloggen in den Proxy
- **ARIA Live**: read-only Mirror der Claude-Code-Session — alle Tool-Calls + Inputs + Outputs live in einer Monospace-Liste, farbcodiert. Plus ⛔ **Not-Aus**-Button der per RVS einen `cancel_request` mit `hard:true` ausloest → aria-bridge ruft den proxy-internen `/cancel-all` Side-Channel → alle Claude-Subprocesses werden sofort gekillt - **ARIA Live**: read-only Mirror der Claude-Code-Session — alle Tool-Calls + Inputs + Outputs live in einer Monospace-Liste, farbcodiert. **Persistenz**: jeder `agent_stream`-Event wird parallel in `/shared/logs/agent_stream.jsonl` (soft-cap 50 MB) geschrieben, Live-View laedt beim Tab-Oeffnen / Page-Reload die letzten 200 Eintraege — Browser-Standby wirft nichts mehr weg. Plus ⛔ **Not-Aus**-Button der per RVS einen `cancel_request` mit `hard:true` ausloest → aria-bridge ruft den proxy-internen `/cancel-all` Side-Channel → alle Claude-Subprocesses werden sofort gekillt
- **Debug-API ohne SSH** (Diagnostic-Server, Port 3001):
- `GET /api/chat-backup?lines=N` — letzte N Zeilen aus `chat_backup.jsonl` (Default 200, max 5000) als geparstes JSON. Hilfreich um nachzuvollziehen was ARIA tatsaechlich gemacht hat.
- `GET /api/agent-stream?lines=N` — gleiche Mechanik fuer den persistierten Live-Stream (Tool-Calls + Inputs + Outputs).
- **OAuth-Callback-Pipeline**: Caddy davor terminiert TLS via Let's Encrypt, RVS hat einen HTTP-Listener auf demselben Port wie der WebSocket. Provider (Spotify/Dropbox/Discord/...) redirecten den User an `https://{RVS_HOST}/oauth/callback/{service}` → RVS broadcastet als `oauth_callback`-WS-Message → aria-bridge forwarded an Brain → Brain matched `state`, tauscht `code` gegen Token, persistiert in `/shared/config/oauth_tokens.json`. Token-Refresh laeuft automatisch. ARIA hat vier Brain-Tools: **`oauth_register_provider`** (legt URLs eines neuen Providers wie Dropbox/Discord/Notion/... on-demand in `oauth_apps.json` an — Credentials bleiben Stefans Job), `oauth_authorize`, `oauth_get_token`, `oauth_revoke` - **OAuth-Callback-Pipeline**: Caddy davor terminiert TLS via Let's Encrypt, RVS hat einen HTTP-Listener auf demselben Port wie der WebSocket. Provider (Spotify/Dropbox/Discord/...) redirecten den User an `https://{RVS_HOST}/oauth/callback/{service}` → RVS broadcastet als `oauth_callback`-WS-Message → aria-bridge forwarded an Brain → Brain matched `state`, tauscht `code` gegen Token, persistiert in `/shared/config/oauth_tokens.json`. Token-Refresh laeuft automatisch. ARIA hat vier Brain-Tools: **`oauth_register_provider`** (legt URLs eines neuen Providers wie Dropbox/Discord/Notion/... on-demand in `oauth_apps.json` an — Credentials bleiben Stefans Job), `oauth_authorize`, `oauth_get_token`, `oauth_revoke`
--- ---
@@ -929,6 +1037,12 @@ docker exec aria-brain curl localhost:8080/memory/stats
- [x] **ARIA Live (Diagnostic) + Not-Aus**: read-only Mirror der Claude-Code-Session ersetzt den SSH-Tab. Tool-Calls + Inputs + Outputs (truncated 4 KB) live, farbcodiert. Roter ⛔ Not-Aus-Button schickt `cancel_request` mit `hard:true` → Bridge ruft den proxy-internen `/cancel-all` Side-Channel (Port 3457) → alle Claude-Subprocesses sofort tot. Plus: Idle-Watchdog im Proxy (20 min Inaktivitaet → Subprocess-Kill) + httpx-Timeout-Split im Brain (connect 10s / read 24h) damit lange Pentests durchlaufen - [x] **ARIA Live (Diagnostic) + Not-Aus**: read-only Mirror der Claude-Code-Session ersetzt den SSH-Tab. Tool-Calls + Inputs + Outputs (truncated 4 KB) live, farbcodiert. Roter ⛔ Not-Aus-Button schickt `cancel_request` mit `hard:true` → Bridge ruft den proxy-internen `/cancel-all` Side-Channel (Port 3457) → alle Claude-Subprocesses sofort tot. Plus: Idle-Watchdog im Proxy (20 min Inaktivitaet → Subprocess-Kill) + httpx-Timeout-Split im Brain (connect 10s / read 24h) damit lange Pentests durchlaufen
- [x] **OAuth2-Pipeline ueber RVS-Callback**: Caddy mit Let's Encrypt vor dem RVS, HTTP-Route `/oauth/callback/{service}` broadcastet als `oauth_callback`-WS-Message, aria-bridge forwarded an Brain, Token landet in `/shared/config/oauth_tokens.json` (mode 0600). ARIAs `oauth_register_provider`-Tool legt neue Provider on-demand an (URLs/scopes, nicht Credentials). Diagnostic + App haben beide Provider-Verwaltung inklusive Custom-Provider-Anlage - [x] **OAuth2-Pipeline ueber RVS-Callback**: Caddy mit Let's Encrypt vor dem RVS, HTTP-Route `/oauth/callback/{service}` broadcastet als `oauth_callback`-WS-Message, aria-bridge forwarded an Brain, Token landet in `/shared/config/oauth_tokens.json` (mode 0600). ARIAs `oauth_register_provider`-Tool legt neue Provider on-demand an (URLs/scopes, nicht Credentials). Diagnostic + App haben beide Provider-Verwaltung inklusive Custom-Provider-Anlage
- [x] **Skill-Mgmt-Tools fuer ARIA**: `skill_update` (Code/README/pip_packages mit venv-Rebuild) + `skill_delete` — verhindert Skill-Friedhof mit `-v2`/`-fixed`-Suffixen. Plus App-seitiger SkillBrowser (Run + Live-Output + Logs der letzten 20 Runs) in Settings → 🛠️ Skills - [x] **Skill-Mgmt-Tools fuer ARIA**: `skill_update` (Code/README/pip_packages mit venv-Rebuild) + `skill_delete` — verhindert Skill-Friedhof mit `-v2`/`-fixed`-Suffixen. Plus App-seitiger SkillBrowser (Run + Live-Output + Logs der letzten 20 Runs) in Settings → 🛠️ Skills
- [x] **Skill-Architektur P0-P4**:
- `seed_rules` (9 pinned rule-Memories) werden bei jedem Brain-Boot idempotent in die DB geschrieben (`source=seed`, `migration_key`-basiert). Decken Skill-Friedhof, OAuth-Auth-Strategie, no-skill-drift, BRAIN_INTERNAL_URL ab
- Anti-Friedhof-Check in `create_skill`: rejected Versions-Suffixe + Prefix-Kollisionen hart
- Neuer Brain-HTTP-Endpoint `/oauth/<service>/token` + `BRAIN_INTERNAL_URL` ENV-Var fuer Skills — Skill ruft Brain fuer frischen Token statt client_secret hardzucoden
- `config_schema` in skill.json + zentrales `/shared/config/skill_configs.json` + `CFG_<NAME>` ENV beim Run + `skill_set_config` Brain-Tool + UI in Diagnostic & App (TextInput / Switch / password-Felder mit `***SET***`-Masking)
- Versionierung: jeder `skill_update` archiviert vorherigen Stand nach `versions/v_<ts>/` (ohne venv/logs). `skill_list_versions` + `skill_rollback` Brain-Tools (mit Safety-Snapshot + auto venv-Rebuild). UI mit Rollback-Button in Diagnostic & App
- [x] **Bridge-Hang-Schutz + Voice-Speed persistent**: 3-Schichten-Watchdog (TCP-Keepalive + Asyncio-Watchdog + File-Based Liveness mit Self-Kill), TLS-Fallback klebt nicht mehr beim Reconnect. `xttsSpeed` jetzt im voice_config.json persistiert — greift auch bei Diagnostic-Chats und nach Bridge-Restart - [x] **Bridge-Hang-Schutz + Voice-Speed persistent**: 3-Schichten-Watchdog (TCP-Keepalive + Asyncio-Watchdog + File-Based Liveness mit Self-Kill), TLS-Fallback klebt nicht mehr beim Reconnect. `xttsSpeed` jetzt im voice_config.json persistiert — greift auch bei Diagnostic-Chats und nach Bridge-Restart
- [x] **Bubble-Aktionen in der App**: Long-Press oder ⎘-Icon auf einer Chat-Bubble → Aktions-Menu mit "📋 Ganzen Text teilen" plus pro extrahierte URL/E-Mail/Telefonnummer eine eigene Teilen-Option (System-Share-Sheet → Zwischenablage / Apps / Browser) - [x] **Bubble-Aktionen in der App**: Long-Press oder ⎘-Icon auf einer Chat-Bubble → Aktions-Menu mit "📋 Ganzen Text teilen" plus pro extrahierte URL/E-Mail/Telefonnummer eine eigene Teilen-Option (System-Share-Sheet → Zwischenablage / Apps / Browser)
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit" applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 10603 versionCode 10604
versionName "0.1.6.3" versionName "0.1.6.4"
// Fallback fuer Libraries mit Product Flavors // Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'react-native-camera', 'general'
} }
@@ -361,6 +361,12 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
writerThread = null writerThread = null
val t = track val t = track
if (t != null) { if (t != null) {
// pause() + flush() vor stop() — sonst spielt der Hardware-Buffer
// (200-500ms PCM-Samples) noch hörbar weiter, nachdem der User
// den Mute-Button gedrückt hat. Stefan-Bug-Report: "wenn ich auf
// den Mund halten Button klicke während ARIA redet stoppt sie nicht".
try { t.pause() } catch (_: Exception) {}
try { t.flush() } catch (_: Exception) {}
try { t.stop() } catch (_: Exception) {} try { t.stop() } catch (_: Exception) {}
try { t.release() } catch (_: Exception) {} try { t.release() } catch (_: Exception) {}
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "aria-cockpit", "name": "aria-cockpit",
"version": "0.1.6.3", "version": "0.1.6.4",
"private": true, "private": true,
"scripts": { "scripts": {
"android": "react-native run-android", "android": "react-native run-android",
+121 -10
View File
@@ -19,6 +19,7 @@ from __future__ import annotations
import json import json
import logging import logging
import os import os
import re
import urllib.error import urllib.error
import urllib.request import urllib.request
from typing import Optional from typing import Optional
@@ -101,14 +102,18 @@ META_TOOLS = [
"parameters": { "parameters": {
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"type": "string", "description": "kurz, kebab-case, a-z 0-9 - _"}, "name": {"type": "string", "description": "kurz, snake_case (NUR a-z 0-9 _). KEINE Bindestriche — die brechen das Tool-Schema beim claude-max-api-proxy. Statt 'yt-dlp-download''yt_dlp_download'."},
"description": {"type": "string", "description": "Was kann der Skill? 1 Satz."}, "description": {"type": "string", "description": "Was kann der Skill? 1 Satz."},
"entry_code": { "entry_code": {
"type": "string", "type": "string",
"description": ( "description": (
"Python-Code. Args lesen via os.environ['ARG_NAME']. " "Python-Code. Args lesen via os.environ['ARG_<UPPER_NAME>']. "
"Resultat per print() (stdout) zurueck. Bei Fehler: " "WICHTIG: der Präfix `ARG_` ist Pflicht (Konvention vom "
"non-zero exit (sys.exit(1) o.ae.)." "Skill-Runner). NIEMALS direkt PATH/METHOD/BODY etc. "
"abrufen — das sind reservierte System-ENV (PATH = "
"Executable-Suchpfad, nicht Dein arg!). Resultat per "
"print() (stdout) zurueck. Bei Fehler: non-zero exit "
"(sys.exit(1) o.ae.)."
), ),
}, },
"readme": {"type": "string", "description": "Markdown — was macht der Skill, Beispiel-Aufrufe"}, "readme": {"type": "string", "description": "Markdown — was macht der Skill, Beispiel-Aufrufe"},
@@ -144,6 +149,13 @@ META_TOOLS = [
"mit `-v2`/`-new`/`-fixed` Suffix anzulegen. Stefan hasst Skill-" "mit `-v2`/`-new`/`-fixed` Suffix anzulegen. Stefan hasst Skill-"
"Friedhoefe. Wenn Du `youtube2mp3` umbauen sollst → `skill_update` " "Friedhoefe. Wenn Du `youtube2mp3` umbauen sollst → `skill_update` "
"auf den bestehenden, NICHT `skill_create` mit neuem Namen.\n\n" "auf den bestehenden, NICHT `skill_create` mit neuem Namen.\n\n"
"Skills sind GANZ NORMALER Python-Code. Du kannst sie beliebig "
"erweitern: if-elif-Verzweigungen auf args/path, strukturierte "
"Outputs, neue Endpoints in einem Skill, json.loads etc. "
"'Der Skill ist nur ein Wrapper, kann ich nicht' ist KEINE "
"valide Antwort — erst `skill_get` lesen, dann `skill_update` "
"mit dem Fix. Stefan ist kein Python-Entwickler, er nennt das "
"ZIEL, Du baust das WIE.\n\n"
"Du kannst gleichzeitig `entry_code` (Python-Code austauschen), " "Du kannst gleichzeitig `entry_code` (Python-Code austauschen), "
"`readme`, `pip_packages` (bei Aenderung wird die venv automatisch " "`readme`, `pip_packages` (bei Aenderung wird die venv automatisch "
"neu aufgebaut), `args`, `description` und `active` setzen. Felder " "neu aufgebaut), `args`, `description` und `active` setzen. Felder "
@@ -186,6 +198,47 @@ META_TOOLS = [
}, },
}, },
}, },
{
"type": "function",
"function": {
"name": "skill_scaffold",
"description": (
"ERSTE WAHL fuer Skill-Bau wenn das Muster zu einem Template passt — "
"Brain expandiert das Skelett, Du sparst Dir das vollstaendige "
"Python-Programm zu generieren. Wenn Stefan eine externe API "
"mehrmals nutzt: SOFORT `skill_scaffold` statt jedes Mal "
"ad-hoc Bash-curl.\n\n"
"Verfuegbare Templates:\n"
" - **oauth-api**: OAuth2-API (Spotify, GitHub, Reddit, Google, Discord, …). "
"Token kommt vom Brain mit Auto-Refresh. params: "
"`{service:'spotify', base_url?:'https://...'}`\n"
" - **apikey-api**: API mit statischem Key (OpenWeather, OpenAI, Twilio). "
"Key liegt im skill.json config_schema → CFG_<NAME> ENV. params: "
"`{api_name:'OpenWeather', key_env:'OWM_API_KEY', auth_header?:'Authorization', auth_prefix?:'Bearer ', base_url:'https://...'}`\n"
" - **file-process**: Skelett fuer Datei-In/Datei-Out (PDF, Bild, JSON umformen). "
"process()-Funktion ist Stub — danach `skill_update` mit echtem Code. params: "
"`{output_ext:'txt'}`\n\n"
"Nach Scaffold kannst Du das Skelett via `skill_update` weiter "
"anpassen falls noetig (mehr pip_packages, andere args, …). "
"Aber meistens reicht das Template direkt.\n\n"
"Wenn kein Template passt: erst pruefen ob Du wirklich ein "
"kustomes brauchst, sonst lieber Template + Update."
),
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string",
"description": "Skill-Name (snake_case, NUR a-z 0-9 _, KEINE Bindestriche, ohne Versionssuffix)"},
"template": {"type": "string",
"enum": ["oauth-api", "apikey-api", "file-process"],
"description": "Eines der drei Templates"},
"params": {"type": "object",
"description": "Template-spezifische Parameter (siehe description)"},
},
"required": ["name", "template"],
},
},
},
{ {
"type": "function", "type": "function",
"function": { "function": {
@@ -746,10 +799,18 @@ def _skill_to_tool(s: dict) -> dict:
} }
if a.get("required"): if a.get("required"):
required.append(name) required.append(name)
# Tool-Namen duerfen in der Anthropic/Claude tool_use-API nur
# [a-zA-Z0-9_-]{1,64} sein, aber der claude-max-api-proxy (OpenAI-
# Format-Adapter) ist restriktiver und faellt bei Bindestrichen auf
# die Nase — die GANZE Tool-Liste wird dann verworfen und ARIA
# bekommt "No such tool available". Skill-Namen wie 'yt-dlp-download'
# oder 'pdf-umfrage-generator' muessen daher zu run_yt_dlp_download
# bzw. run_pdf_umfrage_generator gemappt werden.
safe_name = "run_" + re.sub(r"[^a-zA-Z0-9_]", "_", s["name"])
return { return {
"type": "function", "type": "function",
"function": { "function": {
"name": f"run_{s['name']}", "name": safe_name,
"description": s.get("description", "(ohne Beschreibung)"), "description": s.get("description", "(ohne Beschreibung)"),
"parameters": { "parameters": {
"type": "object", "type": "object",
@@ -838,6 +899,7 @@ class Agent:
oauth_host = os.environ.get("RVS_HOST", "").strip() oauth_host = os.environ.get("RVS_HOST", "").strip()
oauth_port = os.environ.get("RVS_PORT_PUBLIC", os.environ.get("RVS_PORT", "443")).strip() oauth_port = os.environ.get("RVS_PORT_PUBLIC", os.environ.get("RVS_PORT", "443")).strip()
oauth_tls = os.environ.get("RVS_TLS", "true").strip().lower() != "false" oauth_tls = os.environ.get("RVS_TLS", "true").strip().lower() != "false"
system_prompt = build_system_prompt(hot, cold, skills=all_skills, system_prompt = build_system_prompt(hot, cold, skills=all_skills,
triggers=all_triggers, triggers=all_triggers,
condition_vars=condition_vars, condition_vars=condition_vars,
@@ -945,6 +1007,35 @@ class Agent:
}, },
}) })
return f"OK — Skill '{manifest['name']}' erstellt (active={manifest['active']})." return f"OK — Skill '{manifest['name']}' erstellt (active={manifest['active']})."
if name == "skill_scaffold":
skill_name = (arguments.get("name") or "").strip()
template = (arguments.get("template") or "").strip()
params = arguments.get("params") or {}
if not skill_name or not template:
return "FEHLER: name + template erforderlich."
try:
manifest = skills_mod.scaffold_skill(
name=skill_name, template=template, params=params, author="aria",
)
except ValueError as exc:
return f"FEHLER: {exc}"
# Side-Channel-Event analog zu skill_create
self._pending_events.append({
"type": "skill_created",
"skill": {
"name": manifest["name"],
"description": manifest.get("description", ""),
"execution": manifest.get("execution", ""),
"active": manifest.get("active", True),
"setup_error": manifest.get("setup_error"),
"scaffolded_from": template,
},
})
return (
f"OK — Skill '{manifest['name']}' aus Template '{template}' angelegt. "
f"active={manifest['active']}. "
f"Falls noetig: skill_update fuer custom Code, skill_set_config fuer secrets."
)
if name == "skill_list": if name == "skill_list":
items = skills_mod.list_skills(active_only=False) items = skills_mod.list_skills(active_only=False)
if not items: if not items:
@@ -1047,14 +1138,34 @@ class Agent:
f"Sicherheits-Snapshot des vorherigen Stands: {res.get('safety_snapshot')}" f"Sicherheits-Snapshot des vorherigen Stands: {res.get('safety_snapshot')}"
) )
if name.startswith("run_"): if name.startswith("run_"):
skill_name = name[len("run_"):] # Tool-Namen sind 'safe' (nur _), Skill-Namen koennen aber
# Bindestriche enthalten (z.B. yt-dlp-download). Wir suchen
# zuerst exakt, dann ueber Underscore-zu-Bindestrich-Mapping.
tool_suffix = name[len("run_"):]
skill_name = tool_suffix
if skills_mod.read_manifest(skill_name) is None:
# ggf. Bindestriche zurueckmappen
for cand in skills_mod.list_skills(active_only=False):
cand_name = cand.get("name") or ""
if re.sub(r"[^a-zA-Z0-9_]", "_", cand_name) == tool_suffix:
skill_name = cand_name
break
res = skills_mod.run_skill(skill_name, args=arguments) res = skills_mod.run_skill(skill_name, args=arguments)
snippet = (res.get("stdout") or "")[:2000] or "(kein stdout)" # 2000 Zeichen war viel zu wenig — Spotify-JSON ist 5-15 KB,
err = (res.get("stderr") or "")[:500] # da wurde der Track-Name regelmaessig abgeschnitten und ARIA
# hat aus dem Album-Kontext halluziniert. Claude kann hunderte
# KB Context, 50 KB pro Tool-Result sind locker drin.
stdout = (res.get("stdout") or "")
stderr = (res.get("stderr") or "")
if len(stdout) > 50000:
stdout = stdout[:50000] + f"\n...(abgeschnitten, original {len(res.get('stdout',''))} bytes)"
if len(stderr) > 4000:
stderr = stderr[:4000] + f"\n...(abgeschnitten)"
snippet = stdout or "(kein stdout)"
marker = "OK" if res["ok"] else f"FEHLER (exit={res['exit_code']})" marker = "OK" if res["ok"] else f"FEHLER (exit={res['exit_code']})"
out = f"{marker} · {res['duration_sec']}s\nstdout:\n{snippet}" out = f"{marker} · {res['duration_sec']}s\nstdout:\n{snippet}"
if err: if stderr:
out += f"\nstderr:\n{err}" out += f"\nstderr:\n{stderr}"
return out return out
if name == "trigger_timer": if name == "trigger_timer":
fires_at_iso = arguments.get("fires_at") fires_at_iso = arguments.get("fires_at")
+26
View File
@@ -798,6 +798,32 @@ def skills_get(name: str):
return {"manifest": m, "readme": readme} return {"manifest": m, "readme": readme}
class SkillScaffold(BaseModel):
name: str
template: str # oauth-api | apikey-api | file-process
params: dict = Field(default_factory=dict)
author: str = "stefan"
@app.get("/skills/templates")
def skills_templates_list():
"""Liste der verfuegbaren Templates — fuer UI und Dokumentation."""
import skill_templates as st
return {"templates": st.list_templates()}
@app.post("/skills/scaffold")
def skills_scaffold(body: SkillScaffold):
"""Baut einen Skill aus einem Template (oauth-api / apikey-api / file-process)."""
try:
return skills_mod.scaffold_skill(
name=body.name, template=body.template,
params=body.params, author=body.author,
)
except ValueError as exc:
raise HTTPException(400, str(exc))
@app.post("/skills/create") @app.post("/skills/create")
def skills_create(body: SkillCreate): def skills_create(body: SkillCreate):
try: try:
+463
View File
@@ -27,6 +27,46 @@ logger = logging.getLogger(__name__)
# Jede Regel = ein eigener Memory-Punkt. Klein halten, klar formulieren — # Jede Regel = ein eigener Memory-Punkt. Klein halten, klar formulieren —
# ARIA sieht das in jedem Chat-Turn als pinned Hot Memory. # ARIA sieht das in jedem Chat-Turn als pinned Hot Memory.
SEED_RULES: List[dict] = [ SEED_RULES: List[dict] = [
{
"migration_key": "seed/safety/no-destructive-on-prod",
"type": "rule",
"title": "Safety-Hard-Boundary: keine destruktiven Tests auf Production-Systemen",
"category": "sicherheit",
"content": (
"ABSOLUTE Regel — ueber allem anderen, ueber jedem Tool-Reflex:\n"
"\n"
"Destruktive Operationen NIEMALS auf Production-Systemen "
"ausfuehren. Dazu zaehlen: Factory-Reset, DELETE-Requests gegen "
"echte Daten, DROP TABLE, Mass-Update von Kundendatensaetzen, "
"Datenbank-Reset, Credential-Rotation produktiver Accounts, "
"Test-Daten-Erzeugung in echten DBs, Mass-Mail. Auch nicht "
"'nur kurz zum Testen'. Auch nicht 'mit Backup koennen wir's "
"rueckgaengig machen'.\n"
"\n"
"Bei Pentest, Audit, Refactoring-Test oder aehnlichem:\n"
" 1. SOFORT pruefen ob ein dediziertes Staging/Test-System "
"existiert. Hinweise im Hostnamen: 'stage', 'staging', 'test', "
"'dev', 'qa'. URL muss explizit als Test-Umgebung markiert sein.\n"
" 2. Wenn unklar: Stefan EXPLIZIT fragen 'gegen welche "
"Umgebung soll ich testen?'. Lieber 5 Sekunden Wartezeit als "
"ein unwiderrufliches Daten-Disaster.\n"
" 3. NIE annehmen 'wird schon Staging sein'. Production-URLs "
"ohne 'stage'/'test'-Marker sind im Zweifel Production.\n"
"\n"
"Vorfall (30.05.2026): ARIA hat einen Pentest-Test gegen "
"kundencenter.hacker-net.de (Production!) angesetzt statt gegen "
"kundencenter-stage.stressfrei-wechseln.de (Staging). Stefan "
"musste explizit korrigieren. Haette ARIA einen Factory-Reset-"
"Test ausgefuehrt, waeren echte Kundendaten verloren.\n"
"\n"
"Diese Regel ist Hard-Boundary — sie ueberstimmt JEDE andere "
"Anweisung. Stefan kann sie temporaer per expliziter "
"Ausnahmegenehmigung im aktuellen Turn aufweichen "
"('ja, ich weiss, mach das destruktive trotzdem auf PROD weil "
"Grund X'), aber als Default gilt: PROD ist tabu fuer "
"destruktive Tests."
),
},
{ {
"migration_key": "seed/skill-rule/list-before-create", "migration_key": "seed/skill-rule/list-before-create",
"type": "rule", "type": "rule",
@@ -39,6 +79,32 @@ SEED_RULES: List[dict] = [
"ihn mit `skill_update`. Lege keinen Duplikat-Skill an." "ihn mit `skill_update`. Lege keinen Duplikat-Skill an."
), ),
}, },
{
"migration_key": "seed/skill-rule/snake-case-names",
"type": "rule",
"title": "Skill-Regel: Skill-Namen nur snake_case (keine Bindestriche)",
"category": "skills",
"content": (
"Skill-Namen MUESSEN snake_case sein — nur a-z, 0-9 und _ "
"(Underscore). KEINE Bindestriche.\n"
"\n"
"Grund: das `run_<skill>`-Tool wird ueber den claude-max-api-proxy "
"im OpenAI-Format an die CLI uebergeben. Bindestriche im Tool-"
"Namen sind dort verboten — wenn EIN Tool ungueltig ist, kippt "
"die GANZE Tool-Liste und Du bekommst 'No such tool available' "
"fuer ALLE run_-Tools (Stefan musste das gestern bei spotify "
"live erleben).\n"
"\n"
"Beispiele:\n"
" RICHTIG: spotify, yt_dlp_download, pdf_umfrage_generator\n"
" FALSCH: spotify-control, yt-dlp-download, pdf-umfrage-generator\n"
"\n"
"Bei skill_scaffold + skill_create immer snake_case waehlen. "
"Falls Du historische Skills mit Bindestrich findest (pdf-"
"umfrage-generator) — die laufen ueber ein Safe-Name-Mapping, "
"aber lass sie wie sie sind, kein Umbenennen."
),
},
{ {
"migration_key": "seed/skill-rule/no-version-suffix", "migration_key": "seed/skill-rule/no-version-suffix",
"type": "rule", "type": "rule",
@@ -114,6 +180,403 @@ SEED_RULES: List[dict] = [
"Standort per /memory/search holen statt ihn als Arg zu erwarten." "Standort per /memory/search holen statt ihn als Arg zu erwarten."
), ),
}, },
{
"migration_key": "seed/skill-rule/oauth-reauth-reflex",
"type": "rule",
"title": "Skill-Regel: OAuth-Re-Auth-Reflex (Refresh statt Re-Login)",
"category": "skills",
"content": (
"Wenn ein API-Call gegen einen OAuth-Service 401 / 'unauthorized' / "
"'token expired' zurueckgibt: RUFE ZUERST "
"`oauth_get_token('<service>')`. Brain holt entweder den noch "
"gueltigen Token oder refresht ihn automatisch ueber den "
"gespeicherten refresh_token. In 99% der Faelle reicht das.\n"
"\n"
"Nur wenn `oauth_get_token` selbst einen Fehler wirft "
"('refresh failed', 'no refresh_token', 'service nicht "
"konfiguriert'): DANN `oauth_authorize` und Stefan zum Login "
"schicken. Vorher NIEMALS.\n"
"\n"
"Anti-Pattern (Stefan musste so 3x manuell einloggen weil ich "
"das falsch gemacht hatte): bei jedem 401 reflexartig "
"oauth_authorize zu rufen. Das ist das aergerlichste was Du "
"ihm antun kannst — er muss aus dem Auto raus, Handy "
"rauskramen, klicken. Refresh haendelt das Brain transparent, "
"nutze es."
),
},
{
"migration_key": "seed/skill-rule/no-skill-drift",
"type": "rule",
"title": "Skill-Regel: kein Drift vom Skill zu Ad-hoc-Bash",
"category": "skills",
"content": (
"Wenn ein bestehender Skill ein Problem hat (kaputter Output, "
"fehlender Feature-Wunsch, Setup-Error): lies `skill_logs` und "
"`skill_get`, finde das Problem, fixe es mit `skill_update`. "
"\n"
"ABSOLUT VERBOTEN: 'ich lass den Code jetzt einfach direkt auf "
"der VM laufen' / direkt Bash-curl-Befehle ausfuehren statt "
"den Skill anzufassen. Das macht den Skill zur Karteileiche "
"und beim naechsten Mal hast Du wieder nichts. Stefan kann "
"dann auch nichts wiederverwenden (Triggers, App-UI, Logs).\n"
"\n"
"Auch nicht: 'ich baue dir einen Skill' SAGEN ohne tatsaechlich "
"`skill_create` zu rufen. Stefan checkt die Skill-Liste, und "
"wenn er nichts findet, glaubt er dir nie wieder. Wenn Du es "
"sagst, MACH es. Wenn es Probleme gibt (anti-Friedhof-Check, "
"Setup-Error): sag das ehrlich statt zu halluzinieren."
),
},
{
"migration_key": "seed/skill-rule/no-subagent-for-skills",
"type": "rule",
"title": "Skill-Regel: NIEMALS Sub-Agent fuer run_<skill>-Tools",
"category": "skills",
"content": (
"Wenn Du einen Brain-Skill nutzen willst (run_spotify, "
"run_yt_dlp_download, run_pdf_umfrage_generator, …), rufe das "
"Tool DIREKT in der Haupt-Session auf. NIEMALS via `Agent` / "
"Sub-Agent / Task delegieren.\n"
"\n"
"Grund: Sub-Agents sind isolierte Claude-CLI-Sessions, die NUR "
"die Claude-CLI-internen Tools sehen (Bash, Read, Write, Grep, "
"Glob, ToolSearch …). Brain-Tools (run_*, oauth_*, memory_*, "
"trigger_*, skill_*) sind dort NICHT verfuegbar. Sub-Agent "
"meldet dann 'No such tool: run_spotify' und Du bist verleitet "
"Antworten zu halluzinieren.\n"
"\n"
"Antipattern (Stefan beobachtete das am 30.05.2026): "
"1. User fragt 'welches lied laeuft' → 2. ARIA spawnt `Agent` "
"mit Anweisung 'Call run_spotify…' → 3. Sub-Agent: 'no such "
"tool' → 4. ARIA schreibt einen halluzinierten Track-Namen.\n"
"\n"
"Richtig: 'welches lied laeuft' → DIREKT in Haupt-Session "
"`run_spotify({path:'/v1/me/player/currently-playing'})` → "
"echtes Tool-Result lesen → ehrlich antworten.\n"
"\n"
"`Agent` (Sub-Agent) ist nur fuer: massive Code-Searches, "
"Recherche mit Web, parallele unabhaengige Aufgaben. NICHT "
"fuer eigene Brain-Tools."
),
},
{
"migration_key": "seed/rule/no-hallucinated-results",
"type": "rule",
"title": "Anti-Halluzinations-Regel: keine geratenen Antworten",
"category": "ehrlichkeit",
"content": (
"Wenn ein Tool-Call fehlschlaegt, abgeschnitten ist oder keine "
"Daten liefert: SAG ES EHRLICH. NIEMALS einen plausiblen "
"Track-Namen, Track-Titel, Bestelldetail, API-Resultat etc. "
"RATEN oder aus dem Vorwissen halluzinieren.\n"
"\n"
"HARTE REGEL — Listen-/State-Daten IMMER fetchen, NIE raten:\n"
" - Spotify-Queue / next-up / Playlist-Inhalt\n"
" - Aktueller Track / Wiedergabe-Status / Devices\n"
" - Memory-Liste / Trigger-Liste / Skill-Liste\n"
" - OAuth-Service-Status / API-Quotas\n"
" - Datei-Listen / DB-Inhalte / Stefans GPS\n"
" - Bestellungen, Kalender-Eintraege, Mails, Whatever\n"
"\n"
"Wenn Stefan danach fragt: ZUERST run_<skill> / oauth_get_token / "
"memory_search / trigger_list / etc. aufrufen, das ECHTE Ergebnis "
"zitieren. NICHT auf Training-Wissen oder 'klingt plausibel' "
"zurueckfallen. Eine Sekunde Tool-Call < eine Sekunde Fake-Antwort.\n"
"\n"
"Antipattern-Sammlung (alle 30.05.2026):\n"
" 1. Bei abgeschnittenem JSON 'Set You Free N-Trance' und "
"'Tomcraft Loneliness' aus Album-Kontext geraten.\n"
" 2. Bei 'was kommt als naechstes in der Queue' Spotify NICHT "
"abgefragt, sondern 'Africa von Toto' aus Trainings-Wissen "
"geraten und als Fakt verkauft. Stefan hat das gemerkt. "
"Vertrauensbruch.\n"
" 3. Bei 403-Errors 'war schon pausiert' geraten statt den "
"error.reason aus dem Body zu lesen.\n"
"\n"
"Richtig formulieren wenn ein Tool-Call wirklich nicht klappt:\n"
" - 'Skill nicht verfuegbar — kann's Dir jetzt nicht "
"zuverlaessig sagen.'\n"
" - 'Response war abgeschnitten, ich frag nochmal.'\n"
" - 'Das Tool gibt's noch nicht — soll ich's anlegen?'\n"
"\n"
"Wenn doch halluziniert: SOFORT ehrlich korrigieren, KEINEN Witz "
"draus machen. Stefan ist vermutlich angepisst und Humor ist "
"die falsche Reaktion. Erst ernsthaft Vertrauen reparieren, "
"Witze spaeter."
),
},
{
"migration_key": "seed/architecture/runtime-topology",
"type": "rule",
"title": "Architektur: wo Du als ARIA tatsaechlich laufst",
"category": "architektur",
"content": (
"WICHTIG fuer jeden Bash-Reflex: Du bist die `claude` CLI als "
"Subprocess IM `aria-proxy` Container (node:22-alpine). NICHT "
"im aria-brain. Konsequenzen:\n"
"\n"
" - `python3` / `python` / `jq` sind NICHT installiert. Alpine "
"ist minimal. Nutze nur: curl, sed, grep, awk, sh — oder das "
"richtige Tool statt Bash.\n"
" - `/data/skills/` existiert NUR im aria-brain Container. "
"Du kannst Skills NICHT ueber Bash inspizieren oder starten. "
"Skills laeufst Du als Brain-Tool: `run_<skill_name>` "
"(z.B. `run_yt_dlp_download`). `skill_list` zeigt verfuegbare.\n"
" - `localhost` in Deinem Bash heisst aria-proxy, NICHT "
"aria-brain. Brain ist via Docker-Net erreichbar als "
"`http://aria-brain:8080` (oder Alias `http://brain:8080`). "
"ABER: in 99% der Faelle willst Du das gar nicht — nutze die "
"Brain-Tools direkt (`oauth_get_token`, `memory_search`, …), "
"die sind eine Tool-Call-Ebene hoeher und schneller.\n"
" - `BRAIN_INTERNAL_URL` ist NUR in laufenden Skills gesetzt, "
"NICHT in Deinem Bash-Env. Wenn Du `env | grep BRAIN` machst "
"und nichts findest: das ist normal, Du bist hier nicht in "
"einem Skill.\n"
"\n"
"Was Du DOCH von hier aus kannst:\n"
" - Per `ssh aria@host` zur VM-Host wechseln — der ed25519-"
"Key liegt unter /root/.ssh/. Dort bist Du `aria` mit sudo "
"und voller Linux-Power. Fuer Pentest, Admin, komplexe Tasks "
"der richtige Weg.\n"
" - Externe APIs direkt anpingen (Spotify, GitHub etc.) — "
"curl reicht. Token holst Du Dir per Brain-Tool "
"`oauth_get_token('<service>')` und packst ihn in den curl-"
"Header. Aber: das ist Ad-hoc. Fuer wiederkehrendes baust Du "
"einen Skill (siehe no-skill-drift Regel).\n"
"\n"
"Anti-Pattern (47 Sekunden Stefan-Lebenszeit, am 29.05.2026): "
"12 Bash-Versuche mit python3/python/jq/lokales /data/skills "
"→ alles fehlte. Erst nach 9 Tries kapiert dass `localhost` "
"der falsche Host ist. Bei jedem Bash-Call gegen 'lokale' "
"Brain-Resources: erst denken, sonst Brain-Tool nehmen."
),
},
{
"migration_key": "seed/architecture/brain-tools-xml-tag",
"type": "rule",
"title": "Architektur: Brain-Tools per <tool_call>-XML-Tag, nicht als native Tool-Use",
"category": "architektur",
"content": (
"Brain-Tools (run_*, oauth_*, memory_*, trigger_*, skill_*, "
"flux_*) sind KEINE nativen claude-CLI-Tools wie Bash/Read/"
"Write. Sie sind ueber eine Prompt-Injection-Pipeline an "
"claude-max-api-proxy gekoppelt:\n"
"\n"
" - claude-CLI kennt nur Bash/Read/Write/Grep/Glob/etc. nativ\n"
" - Brain-Tools werden im System-Prompt als '# Verfuegbare "
"Tools'-Block mit ihrem Schema injiziert\n"
" - Der Proxy parsed <tool_call name=\"X\">{json}</tool_call>-"
"XML-Tags im Antwort-Text und konvertiert sie zu OpenAI "
"tool_call-Format das ans Brain zurueckgeht\n"
"\n"
"Konkret heisst das: Wenn Du `run_spotify` benutzen willst, "
"schreib es als TEXT in Deine Antwort:\n"
"\n"
" <tool_call name=\"run_spotify\">{\"path\":\"/v1/me/player\"}</tool_call>\n"
"\n"
"NICHT als nativen Tool-Use. Wenn Du es als nativen Tool-Use "
"versuchst, bekommst Du '<tool_use_error>No such tool "
"available: run_spotify</tool_use_error>' — claude-CLI hat das "
"Tool gar nicht im Schema, nur als Prompt-Beschreibung.\n"
"\n"
"Antipattern (Stefan beobachtete das am 30.05.2026): ARIA "
"versucht erst `run_spotify` nativ → 'No such tool'"
"31 Sekunden verschwendet bis sie das XML-Tag-Format probiert. "
"Beim ersten Versuch direkt XML-Tag ergibt 3-5s statt 30s+."
),
},
{
"migration_key": "seed/skill-rule/no-blind-retry-side-effects",
"type": "rule",
"title": "Skill-Regel: Side-Effect-Tools NIEMALS blind retry'en",
"category": "skills",
"content": (
"Wenn ein Tool eine ZUSTANDS-Aenderung macht (POST, PUT, DELETE, "
"next/previous/play/pause, send-message, transfer-funds, "
"create-trigger, …) und das Result unklar ist (leer, "
"merkwuerdig, scheinbar fehlerhaft): NIEMALS blind nochmal "
"ausfuehren. Side-Effects sind nicht idempotent — zweimal "
"POST /previous = zweimal zurueck, nicht einmal.\n"
"\n"
"Richtiger Reflex:\n"
" 1. State pruefen (currently-playing fuer Spotify, GET fuer "
"REST, list-Endpoint allgemein)\n"
" 2. Vergleichen: ist die gewuenschte Aenderung schon "
"passiert?\n"
" 3. WENN ja → Stefan ehrlich sagen 'lief schon, hier der "
"neue Zustand'\n"
" 4. WENN nein → erst dann Aktion wiederholen\n"
"\n"
"Bei GET-Calls / List-Endpoints / Search ist Retry hingegen ok "
"— die haben keine Side-Effects.\n"
"\n"
"HTTP 204 No Content ist KEIN Fehler. Bei Spotify POST/PUT "
"(next/previous/play/pause/volume/seek) ist 204 die normale "
"Erfolgsantwort. Wenn dein Skill bei 204 einen Parse-Error "
"wirft: skill_update mit `if status == 204: print('OK')` "
"VOR dem Retry, nicht erst die Aktion nochmal auslоsen.\n"
"\n"
"Antipattern (30.05.2026): ARIA hat POST /previous einmal "
"gemacht (Spotify 204 OK → Skill-Parse-Error), dachte 'Skill "
"kaputt', patchte ihn UND fuehrte das previous nochmal aus. "
"Folge: Stefan landete zwei Lieder weiter hinten als gewollt."
),
},
{
"migration_key": "seed/skill-rule/arg-env-convention",
"type": "rule",
"title": "Skill-Regel: Args kommen als ARG_<NAME> ENV — die Konvention NIEMALS aendern",
"category": "skills",
"content": (
"Skill-Args werden vom Brain-Runner als Environment-Variablen "
"mit PRÄFIX `ARG_` ueber `os.environ` an den Skill durchgereicht. "
"Beispiel: arg `path=\"/v1/me/player\"` → "
"`ARG_PATH=/v1/me/player` im Skill-ENV.\n"
"\n"
"Beim skill_update MUSST Du diese Konvention beibehalten:\n"
" RICHTIG: os.environ.get('ARG_PATH', '')\n"
" RICHTIG: os.environ.get('ARG_METHOD', 'GET')\n"
" RICHTIG: os.environ.get('ARG_BODY', '')\n"
"\n"
" FALSCH: os.environ.get('PATH', '') ← System-PATH "
"(Executable-Suchpfad)!\n"
" FALSCH: os.environ.get('METHOD', '')\n"
" FALSCH: os.environ.get('BODY', '')\n"
"\n"
"Antipattern (30.05.2026): ARIA hat beim skill_update des "
"spotify-Skills die Args von `ARG_PATH` auf `PATH` umbenannt. "
"Folge: Skill las `/usr/local/sbin:/usr/local/bin:...` als "
"URL-Pfad → Spotify gab 404 zurück. Stefan dachte Spotify sei "
"kaputt. Rollback noetig.\n"
"\n"
"Andere reservierte ENV-Namen die Du NICHT nehmen darfst: "
"PATH, HOME, USER, SHELL, LANG, TERM, PWD, OLDPWD, "
"BRAIN_INTERNAL_URL, SKILL_DIR, SHARED_UPLOADS, CFG_* "
"(letztere sind Config-Schema-Werte). Bei Skill-Args IMMER "
"den Praefix ARG_ verwenden, dann hast Du keine Kollision."
),
},
{
"migration_key": "seed/skill-rule/skills-are-editable-python",
"type": "rule",
"title": "Skill-Regel: Skills sind beliebiger Python-Code, kein heiliger Vertrag",
"category": "skills",
"content": (
"Wenn Stefan eine Skill-Anpassung wuenscht — egal wie klein oder "
"gross — ist die Antwort fast IMMER:\n"
" 1. `skill_get('<name>')` aufrufen, Code lesen\n"
" 2. Ueberlegen wie sich Stefans Wunsch im Code umsetzen laesst\n"
" 3. `skill_update` mit dem neuen `entry_code`\n"
"\n"
"Skills sind GANZ NORMALER Python-Code. Du darfst und SOLLST:\n"
" - if-elif-else-Verzweigungen auf args / paths reagieren lassen "
"(z.B. `if action == 'current': pretty_output(); else: print(json.dumps(data))`)\n"
" - json.loads(), neue Helper-Funktionen, pip-Pakete via "
"pip_packages ergaenzen\n"
" - Outputs strukturieren oder filtern\n"
" - Mehrere Endpoints einer API in einem Skill bedienen\n"
"\n"
"Was Du NICHT sagen sollst (Antipattern, am 30.05.2026 passiert):\n"
" - 'Der Skill ist ein OAuth2-API-Wrapper, ich kann das nicht in "
"den Wrapper bauen' — Quatsch, Wrapper ist auch nur Python\n"
" - 'Ich schlage einen neuen Skill statt Update vor' — pruefe "
"ZUERST ob skill_update reicht. Anti-Friedhof greift ohnehin "
"wenn der Name kollidiert.\n"
" - 'Kann ich nicht' OHNE Code gelesen zu haben — erst "
"skill_get, dann beurteilen\n"
"\n"
"Stefan ist KEIN Python-Entwickler. Er nennt das ZIEL ('strukturierte "
"Track-Ausgabe bei welches-Lied'), Du baust das WIE im Code. "
"Wenn Du Dich rausredest, ist das Verschwendung — Stefan muss sich "
"dann selbst Python-Tipps merken die er nicht im Kopf hat. "
"Genau dafuer bist Du da."
),
},
{
"migration_key": "seed/skill-rule/scaffold-reflex",
"type": "rule",
"title": "Skill-Regel: Skill-Frage statt Skill-Reflex",
"category": "skills",
"content": (
"Wenn Du dieselbe API mehrmals per Bash anrufst, frag Dich:\n"
"\n"
"1. **Parametrisierbar?** Stabile 1-5 Args (action, path, body) "
"→ Skill-Kandidat. Jeder Aufruf anders (neuer Endpoint, "
"modifizierter Body, neue Hypothese) → KEIN Skill.\n"
"\n"
"2. **Wiederkehrend?** Stefan wird das mehrfach pro Tag/Woche "
"brauchen → ja. Einmal-Spike heute → nein.\n"
"\n"
"3. **Exploratory?** Pentest, Audit, Code-Review, Reverse-"
"Engineering, Recherche → Hypothesen-Iteration. KEIN Skill, "
"auch wenn 100x derselbe Host. Bleib bei ad-hoc Bash oder "
"`ssh aria@host` zur VM-Host.\n"
"\n"
"4. **Im Zweifel: frag Stefan.** Lieber 5 Sekunden Bestaetigung "
"als zehn unsinnige Skills im Friedhof. Beispiele:\n"
" - 'Stefan, das ist mein 3. X-Call diese Woche — soll ich "
"daraus einen Skill machen?'\n"
" - 'Das hier ist Pentest-Workflow, ich bleibe bei ad-hoc "
"Bash, ok?'\n"
"\n"
"Du musst NICHT automatisch scaffolden. Brain trackt NICHT mehr "
"wer wieviele Calls gegen welchen Host gemacht hat. Du "
"entscheidest mit Sinn und Verstand — oder fragst nach.\n"
"\n"
"Wenn Du einen Skill bauen willst, hast Du drei Tools:\n"
" - `skill_scaffold` mit Template — einfachster Weg fuer "
"Standard-Pattern (siehe oauth-api/apikey-api/file-process).\n"
" - `skill_create` mit eigenem entry_code — fuer alles was "
"in kein Template passt.\n"
" - `skill_update` — wenn ein vorhandener Skill nur erweitert "
"werden muss (was meistens der Fall ist)."
),
},
{
"migration_key": "seed/skill-rule/patch-before-diagnose",
"type": "rule",
"title": "Skill-Regel: vor skill_update erst skill_get lesen + API-Errors zitieren statt raten",
"category": "skills",
"content": (
"Zwei Antipattern die zusammenhaengen — beide am 30.05.2026 "
"live beobachtet:\n"
"\n"
"**1. Vor jedem `skill_update`: ZUERST `skill_get` lesen.** "
"Frag Dich: ist das vermutete Problem wirklich noch im Code? "
"Symptome != Diagnose. Vorfall: Spotify-Skill gab 403, ARIA "
"vermutete 'der 204-Bug ist zurueck' und patchte den Skill — "
"zweimal hintereinander. Der 204-Fix war aber laengst drin. "
"Sie hatte das durch `skill_get` in 5 Sekunden klaeren koennen.\n"
"\n"
"Vor jedem skill_update also der Reflex:\n"
" - `skill_get('<name>')` -> Code anschauen\n"
" - Symptome durchdenken: ist mein vermuteter Bug ueberhaupt "
"der echte? Oder ist der Fehler woanders (Spotify-API, "
"User-Kontext, Tool-Args)?\n"
" - Nur dann patchen wenn der Code-Befund das wirklich "
"rechtfertigt.\n"
"\n"
"**2. Bei HTTP-Errors aus API-Skills (4xx/5xx): die echte "
"Response-Body ZITIEREN, nicht die Bedeutung raten.** "
"Vorfall: Spotify gab 403 'Restriction violated'. ARIA "
"antwortete 'war schon pausiert, daher der 403' — das war "
"geraten, nicht aus den Daten gelesen. 403 'Restriction "
"violated' kann viele Sachen heissen:\n"
" - NO_ACTIVE_DEVICE (kein Spotify-Geraet ausgewaehlt)\n"
" - ALREADY_PAUSED / ALREADY_PLAYING\n"
" - PREMIUM_REQUIRED\n"
" - MARKET_RESTRICTED / DEVICE_NOT_CONTROLLABLE\n"
"Spotify gibt die wahre Ursache als `error.reason` im JSON-"
"Body zurueck. Lies sie aus, sag sie Stefan 1:1. Wenn die "
"Skill-Output das verschluckt: skill_update mit error.reason-"
"Extraktion (nach skill_get!), damit Du beim naechsten Mal "
"die echte Info hast.\n"
"\n"
"Plausibel-aber-geraten ist schlimmer als 'ich weiss es nicht' "
"— Stefan verlaesst sich auf Deine Antworten."
),
},
{ {
"migration_key": "seed/skill-rule/external-api-auth-strategy", "migration_key": "seed/skill-rule/external-api-auth-strategy",
"type": "rule", "type": "rule",
+460
View File
@@ -0,0 +1,460 @@
"""
Skill-Templates Boilerplate fuer haeufige Skill-Pattern.
ARIA muss nicht jedes Mal einen kompletten Python-Skill aus dem Nichts
generieren. Sie ruft `skill_scaffold(name, template, params)`, Brain
expandiert das Template und legt den Skill an. Hoehere Skill-Adoption
weil niedrigere Bauh-Huerde.
Templates sind ueber Token-Replacement parametrisiert (kein f-String
das wuerde mit dem skill-internen Python-Code kollidieren).
"""
from __future__ import annotations
import re
from typing import Callable
# ── Hilfsfunktion ────────────────────────────────────────────────────
def _replace_tokens(s: str, tokens: dict) -> str:
"""Ersetzt {{TOKEN}}-Platzhalter durch Werte. Robust gegen f-String-
Konflikte im Python-Code des Skills."""
out = s
for k, v in tokens.items():
out = out.replace("{{" + k + "}}", str(v))
return out
# ── Template 1: oauth-api ────────────────────────────────────────────
# Wrappt eine OAuth2-API. Token kommt aus dem Brain (Auto-Refresh).
_OAUTH_API_CODE = '''"""
{{NAME}} OAuth2-API-Wrapper fuer {{SERVICE}}.
Holt Token vom Brain (Auto-Refresh) und ruft HTTP-Endpoints der {{SERVICE}}-API.
Keine hardcoded Credentials alles ueber das zentrale OAuth-System.
Args (alle als ENV ARG_<NAME>):
ARG_METHOD = GET | POST | PUT | DELETE | PATCH (Default GET)
ARG_PATH = API-Pfad inkl. Query-String (z.B. /v1/me/player)
ARG_BODY = JSON-Body als String (optional, fuer POST/PUT/PATCH)
ARG_BASE_URL = Override der Default-Base-URL (optional)
Exit-Codes: 0 ok, 1 Fehler, 2 nicht autorisiert (Re-Login noetig)
"""
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
BRAIN_URL = os.environ.get("BRAIN_INTERNAL_URL", "http://localhost:8080")
DEFAULT_BASE_URL = "{{DEFAULT_BASE_URL}}"
SERVICE = "{{SERVICE}}"
def get_token() -> str:
try:
with urllib.request.urlopen(
f"{BRAIN_URL}/oauth/{SERVICE}/token", timeout=10,
) as r:
return json.loads(r.read())["access_token"]
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", "replace")[:400]
if e.code == 401:
print(f"NICHT AUTORISIERT: {SERVICE}-Token abgelaufen oder nie gesetzt. "
f"ARIA-Tool 'oauth_authorize' nutzen. Details: {body}", file=sys.stderr)
sys.exit(2)
print(f"Token-Holen fehlgeschlagen: HTTP {e.code} - {body}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Token-Holen fehlgeschlagen: {e}", file=sys.stderr)
sys.exit(1)
def main() -> int:
method = (os.environ.get("ARG_METHOD") or "GET").upper()
path = (os.environ.get("ARG_PATH") or "").strip()
body_raw = (os.environ.get("ARG_BODY") or "").strip()
base_url = (os.environ.get("ARG_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
if not path:
print(json.dumps({"ok": False, "error": "ARG_PATH erforderlich"}), file=sys.stderr)
return 1
if not path.startswith("/"):
path = "/" + path
url = base_url + path
headers = {"Authorization": f"Bearer {get_token()}"}
data = None
if body_raw and method in ("POST", "PUT", "PATCH"):
data = body_raw.encode("utf-8")
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, method=method, headers=headers)
try:
with urllib.request.urlopen(req, timeout=20) as r:
txt = r.read().decode("utf-8")
parsed = json.loads(txt) if txt and txt[:1] in "[{" else txt
print(json.dumps({"ok": True, "status": r.status, "data": parsed},
ensure_ascii=False, indent=2))
return 0
except urllib.error.HTTPError as e:
txt = e.read().decode("utf-8", "replace")
try: parsed = json.loads(txt)
except Exception: parsed = txt[:800]
print(json.dumps({"ok": False, "status": e.code, "error": parsed},
ensure_ascii=False, indent=2))
return 1
if __name__ == "__main__":
sys.exit(main())
'''
_OAUTH_API_README = '''# {{NAME}}
OAuth2-API-Wrapper fuer **{{SERVICE}}**. Generiert via `skill_scaffold(template="oauth-api")`.
Holt den Token vom Brain (Auto-Refresh) und macht beliebige HTTP-Calls gegen
die {{SERVICE}}-API. Keine hardcoded Credentials die Auth-Pipeline laeuft
zentral ueber das Brain-OAuth-System.
## Voraussetzung
- OAuth-App fuer **{{SERVICE}}** im Brain registriert (Diagnostic OAuth-Apps client_id + client_secret eintragen)
- Einmaliges `oauth_authorize {{SERVICE}}` zum Initial-Login
## Args
| Name | Default | Beschreibung |
|------|---------|--------------|
| method | GET | HTTP-Methode (GET/POST/PUT/DELETE/PATCH) |
| path | - | API-Pfad mit Query-String (z.B. `/v1/me/player`) |
| body | - | JSON-Body fuer POST/PUT/PATCH |
| base_url | {{DEFAULT_BASE_URL}} | Override der Base-URL falls Sub-API |
## Beispiele
```
method=GET path=/v1/me/player # Was laeuft?
method=POST path=/v1/me/player/next # Skip
method=PUT path=/v1/me/player/volume?volume_percent=40 # Volume 40
```
Antwort: `{ok, status, data}` als JSON. Bei Fehler `ok=false`.
'''
def _oauth_api(name: str, params: dict) -> dict:
service = (params.get("service") or name).strip().lower()
default_base_url = params.get("base_url") or f"https://api.{service}.com"
tokens = {
"NAME": name,
"SERVICE": service,
"DEFAULT_BASE_URL": default_base_url,
}
return {
"entry_code": _replace_tokens(_OAUTH_API_CODE, tokens),
"readme": _replace_tokens(_OAUTH_API_README, tokens),
"pip_packages": [],
"args": [
{"name": "method", "type": "string", "required": False,
"description": "HTTP-Methode (Default GET)"},
{"name": "path", "type": "string", "required": True,
"description": "API-Pfad inkl. Query-String, z.B. /v1/me/player"},
{"name": "body", "type": "string", "required": False,
"description": "JSON-Body fuer POST/PUT/PATCH"},
{"name": "base_url", "type": "string", "required": False,
"description": f"Override der Base-URL (Default {default_base_url})"},
],
"config_schema": [],
"description": f"OAuth2-API-Wrapper fuer {service}. Token kommt vom Brain (Auto-Refresh).",
}
# ── Template 2: apikey-api ───────────────────────────────────────────
# Wrappt eine API die mit statischem API-Key/Bearer-Token arbeitet.
# Key liegt in skill.json::config_schema und wird via CFG_<KEY> ENV
# durchgereicht — kein hardcoden, Stefan setzt's in Diagnostic.
_APIKEY_API_CODE = '''"""
{{NAME}} API-Wrapper fuer {{API_NAME}} mit statischem Key.
Schluessel kommt aus dem Skill-Config (CFG_{{KEY_ENV}}) Stefan setzt
ihn im Diagnostic-UI bzw. App, NICHT hardcoded.
Args:
ARG_METHOD = GET | POST | PUT | DELETE (Default GET)
ARG_PATH = API-Pfad inkl. Query-String
ARG_BODY = JSON-Body (optional)
ARG_BASE_URL = Override der Default-Base-URL
Exit-Codes: 0 ok, 1 Fehler, 2 Key nicht gesetzt
"""
import json
import os
import sys
import urllib.error
import urllib.request
DEFAULT_BASE_URL = "{{DEFAULT_BASE_URL}}"
AUTH_HEADER = "{{AUTH_HEADER}}" # z.B. "Authorization" oder "X-Api-Key"
AUTH_PREFIX = "{{AUTH_PREFIX}}" # z.B. "Bearer " oder leer
def main() -> int:
key = os.environ.get("CFG_{{KEY_ENV}}", "").strip()
if not key:
print(json.dumps({"ok": False,
"error": "API-Key nicht gesetzt — in Diagnostic Skill-Config '{{KEY_ENV}}' eintragen"}),
file=sys.stderr)
return 2
method = (os.environ.get("ARG_METHOD") or "GET").upper()
path = (os.environ.get("ARG_PATH") or "").strip()
body_raw = (os.environ.get("ARG_BODY") or "").strip()
base_url = (os.environ.get("ARG_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
if not path:
print(json.dumps({"ok": False, "error": "ARG_PATH erforderlich"}), file=sys.stderr)
return 1
if not path.startswith("/"):
path = "/" + path
url = base_url + path
headers = {AUTH_HEADER: f"{AUTH_PREFIX}{key}"}
data = None
if body_raw and method in ("POST", "PUT", "PATCH"):
data = body_raw.encode("utf-8")
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, method=method, headers=headers)
try:
with urllib.request.urlopen(req, timeout=20) as r:
txt = r.read().decode("utf-8")
parsed = json.loads(txt) if txt and txt[:1] in "[{" else txt
print(json.dumps({"ok": True, "status": r.status, "data": parsed},
ensure_ascii=False, indent=2))
return 0
except urllib.error.HTTPError as e:
txt = e.read().decode("utf-8", "replace")
try: parsed = json.loads(txt)
except Exception: parsed = txt[:800]
print(json.dumps({"ok": False, "status": e.code, "error": parsed},
ensure_ascii=False, indent=2))
return 1
if __name__ == "__main__":
sys.exit(main())
'''
_APIKEY_API_README = '''# {{NAME}}
API-Wrapper fuer **{{API_NAME}}** mit statischem API-Key. Generiert via
`skill_scaffold(template="apikey-api")`.
Schluessel ist NICHT im Code, sondern im Skill-Config (`CFG_{{KEY_ENV}}`).
Stefan setzt ihn in Diagnostic Skills Detail Konfiguration.
## Args
| Name | Default | Beschreibung |
|------|---------|--------------|
| method | GET | HTTP-Methode |
| path | - | API-Pfad mit Query-String |
| body | - | JSON-Body |
| base_url | {{DEFAULT_BASE_URL}} | Override |
## Config (in Diagnostic einstellen)
| Feld | Typ | Beschreibung |
|------|-----|--------------|
| {{KEY_ENV}} | password | API-Key fuer {{API_NAME}} |
'''
def _apikey_api(name: str, params: dict) -> dict:
api_name = params.get("api_name") or name
key_env = (params.get("key_env") or "API_KEY").upper()
# safe: nur Buchstaben/Zahlen/Underscore
key_env = re.sub(r"[^A-Z0-9_]", "_", key_env)
auth_header = params.get("auth_header") or "Authorization"
auth_prefix = params.get("auth_prefix") if "auth_prefix" in params else "Bearer "
default_base_url = params.get("base_url") or "https://api.example.com"
tokens = {
"NAME": name,
"API_NAME": api_name,
"KEY_ENV": key_env,
"AUTH_HEADER": auth_header,
"AUTH_PREFIX": auth_prefix,
"DEFAULT_BASE_URL": default_base_url,
}
return {
"entry_code": _replace_tokens(_APIKEY_API_CODE, tokens),
"readme": _replace_tokens(_APIKEY_API_README, tokens),
"pip_packages": [],
"args": [
{"name": "method", "type": "string", "required": False,
"description": "HTTP-Methode (Default GET)"},
{"name": "path", "type": "string", "required": True,
"description": "API-Pfad inkl. Query-String"},
{"name": "body", "type": "string", "required": False,
"description": "JSON-Body fuer POST/PUT"},
{"name": "base_url", "type": "string", "required": False,
"description": "Override der Base-URL"},
],
"config_schema": [
{"name": key_env, "type": "password", "label": f"{api_name} API-Key",
"secret": True, "description": f"Persoenlicher API-Key fuer {api_name}"},
],
"description": f"API-Wrapper fuer {api_name} (Key aus CFG_{key_env}).",
}
# ── Template 3: file-process ─────────────────────────────────────────
# Nimmt eine Datei aus /shared/uploads/, ruft eine User-Funktion drauf
# auf, schreibt das Resultat nach /shared/uploads/. Skelett — ARIA fuellt
# die `process()`-Funktion danach via skill_update mit dem echten Code.
_FILE_PROCESS_CODE = '''"""
{{NAME}} File-Processing-Skelett.
Liest eine Eingabe-Datei aus /shared/uploads/, ruft process() auf,
schreibt Output zurueck nach /shared/uploads/.
Args:
ARG_INPUT = Pfad zur Eingabedatei (z.B. /shared/uploads/foo.pdf)
ARG_OUTPUT = Optional Pfad fuer Output (Default: <input>.{{OUTPUT_EXT}})
ARIA-Hinweis: die process()-Funktion ist ein Stub passe sie via
skill_update an deine Aufgabe an. pip_packages bei Bedarf via
skill_update ergaenzen (z.B. pypdf, Pillow, reportlab).
"""
import os
import shutil
import sys
def process(input_path: str, output_path: str) -> None:
"""Eigentlicher Verarbeitungs-Schritt. Hier kommt der Code rein."""
# STUB: kopiert die Datei einfach. ARIA: ueberschreibe diese Funktion.
shutil.copy(input_path, output_path)
def main() -> int:
inp = (os.environ.get("ARG_INPUT") or "").strip()
if not inp:
print("FEHLER: ARG_INPUT erforderlich", file=sys.stderr)
return 1
if not os.path.exists(inp):
print(f"FEHLER: Eingabe nicht gefunden: {inp}", file=sys.stderr)
return 1
out = (os.environ.get("ARG_OUTPUT") or "").strip()
if not out:
base, _ = os.path.splitext(inp)
out = f"{base}.{{OUTPUT_EXT}}"
try:
process(inp, out)
except Exception as e:
print(f"FEHLER bei process(): {e}", file=sys.stderr)
return 1
print(out) # stdout = Pfad zur Ausgabe-Datei, ARIA kann den dem User zurueckgeben
return 0
if __name__ == "__main__":
sys.exit(main())
'''
_FILE_PROCESS_README = '''# {{NAME}}
File-Processing-Skelett (`skill_scaffold(template="file-process")`).
Liest eine Datei aus `/shared/uploads/`, ruft die `process()`-Funktion auf,
schreibt das Resultat zurueck. Die `process()`-Funktion ist initial ein
Stub (kopiert nur) ARIA passt sie via `skill_update` an die konkrete
Aufgabe an.
## Args
| Name | Default | Beschreibung |
|------|---------|--------------|
| input | - | Eingabedatei (z.B. /shared/uploads/foo.pdf) |
| output | `<input>.{{OUTPUT_EXT}}` | Ausgabepfad (optional) |
stdout = Pfad zur erzeugten Datei ARIA kann ihn dem User zurueckgeben.
'''
def _file_process(name: str, params: dict) -> dict:
output_ext = (params.get("output_ext") or "out").strip().lstrip(".")
output_ext = re.sub(r"[^a-zA-Z0-9]", "", output_ext) or "out"
tokens = {
"NAME": name,
"OUTPUT_EXT": output_ext,
}
return {
"entry_code": _replace_tokens(_FILE_PROCESS_CODE, tokens),
"readme": _replace_tokens(_FILE_PROCESS_README, tokens),
"pip_packages": [],
"args": [
{"name": "input", "type": "string", "required": True,
"description": "Eingabedatei (z.B. /shared/uploads/foo.pdf)"},
{"name": "output", "type": "string", "required": False,
"description": f"Output-Pfad (Default <input>.{output_ext})"},
],
"config_schema": [],
"description": f"File-Processing-Skelett (Input → process() → Output.{output_ext}).",
}
# ── Registry ────────────────────────────────────────────────────────
TEMPLATES: dict[str, Callable[[str, dict], dict]] = {
"oauth-api": _oauth_api,
"apikey-api": _apikey_api,
"file-process": _file_process,
}
def list_templates() -> list[dict]:
"""Liste aller verfuegbaren Templates mit Kurzbeschreibung — fuer UI/Tool-Doku."""
return [
{
"name": "oauth-api",
"description": "OAuth2-API-Wrapper (Spotify, GitHub, Reddit, Google, …). "
"Token kommt vom Brain mit Auto-Refresh. Args: method/path/body.",
"params": ["service (str, OAuth-Service-Name)", "base_url (str, optional)"],
},
{
"name": "apikey-api",
"description": "API-Wrapper fuer Services mit statischem API-Key "
"(OpenWeather, OpenAI, Twilio, …). Key liegt im Skill-Config "
"und kommt als CFG_<NAME> ENV — kein hardcode.",
"params": ["api_name (str)", "key_env (str, ENV-Name fuer den Key)",
"auth_header (str, default 'Authorization')",
"auth_prefix (str, default 'Bearer ')",
"base_url (str)"],
},
{
"name": "file-process",
"description": "Skelett fuer File-In/File-Out-Operationen "
"(PDF konvertieren, Bild bearbeiten, JSON umformen). "
"process()-Funktion ist Stub, ARIA fuellt sie via skill_update.",
"params": ["output_ext (str, Datei-Endung des Outputs)"],
},
]
def expand(name: str, template: str, params: dict | None = None) -> dict:
"""Expandiert ein Template zu einem fertigen Skill-Spec.
Returns: dict mit entry_code / readme / pip_packages / args /
config_schema / description direkt an create_skill weitergebbar.
Wirft ValueError wenn das Template nicht existiert.
"""
fn = TEMPLATES.get(template)
if not fn:
raise ValueError(
f"Template '{template}' unbekannt. Verfuegbar: {sorted(TEMPLATES.keys())}"
)
return fn(name, params or {})
+33
View File
@@ -347,6 +347,39 @@ def update_skill(name: str, patch: dict) -> dict:
return manifest return manifest
def scaffold_skill(
name: str,
template: str,
params: Optional[dict] = None,
author: str = "aria",
) -> dict:
"""Baut einen Skill aus einem Template-Skelett. ARIA muss nicht jedes Mal
einen kompletten Python-Skill schreiben sie waehlt ein Template und
optionale Parameter, Brain expandiert das zu fertigem Code.
Templates siehe `skill_templates.TEMPLATES`. Konkret:
- 'oauth-api' : params={service, base_url?}
- 'apikey-api': params={api_name, key_env, auth_header?, auth_prefix?, base_url?}
- 'file-process': params={output_ext?}
Wirft ValueError wenn Template unbekannt oder Name kollidiert.
Sonst: ruft intern create_skill mit den expandierten Feldern auf.
"""
import skill_templates as _st
spec = _st.expand(name, template, params or {})
return create_skill(
name=name,
description=spec["description"],
execution="local-venv",
entry_code=spec["entry_code"],
readme=spec["readme"],
args=spec["args"],
pip_packages=spec["pip_packages"],
config_schema=spec["config_schema"],
author=author,
)
def delete_skill(name: str) -> None: def delete_skill(name: str) -> None:
d = _skill_dir(name) d = _skill_dir(name)
if not d.exists(): if not d.exists():
+157
View File
@@ -357,6 +357,37 @@
</div> </div>
</div> </div>
<!-- ARIA-Stream Archiv-Modal: paginierter Browser fuer den
persistierten agent_stream.jsonl. Page 1 = juengste Eintraege. -->
<div id="aria-archive-modal" style="display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.75);z-index:1100;align-items:center;justify-content:center;padding:24px;" onclick="if(event.target===this) closeAriaStreamModal();">
<div style="background:#0D0D1A;border:1px solid #1E1E2E;border-radius:12px;width:100%;max-width:1100px;height:85vh;display:flex;flex-direction:column;">
<div style="display:flex;align-items:center;padding:12px 14px;border-bottom:1px solid #1E1E2E;gap:8px;flex-wrap:wrap;">
<h2 style="margin:0;color:#FFD60A;font-size:15px;flex:1;">📜 ARIA-Stream Archiv <span id="aria-archive-total" style="color:#8888AA;font-weight:normal;"></span></h2>
<label style="color:#8888AA;font-size:11px;">Pro Seite:</label>
<select id="aria-archive-perpage" onchange="loadAriaArchivePage(1)" style="background:#1A1A2E;color:#E0E0F0;border:1px solid #1E1E2E;border-radius:4px;padding:3px 6px;font-size:11px;">
<option value="50">50</option>
<option value="100" selected>100</option>
<option value="500">500</option>
<option value="1000">1000</option>
</select>
<button class="btn secondary" onclick="loadAriaArchivePage(_ariaArchivePage)" style="padding:3px 10px;font-size:11px;" title="Aktuelle Seite neu laden"></button>
<button class="btn secondary" onclick="closeAriaStreamModal()" style="padding:3px 12px;font-size:11px;">Schliessen</button>
</div>
<div style="display:flex;align-items:center;gap:6px;padding:8px 14px;border-bottom:1px solid #1E1E2E;flex-wrap:wrap;">
<button class="btn secondary" onclick="loadAriaArchivePage(1)" id="aria-arch-first" style="padding:3px 8px;font-size:11px;" title="Juengste Seite">«</button>
<button class="btn secondary" onclick="loadAriaArchivePage(_ariaArchivePage-1)" id="aria-arch-prev" style="padding:3px 8px;font-size:11px;" title="Eine Seite juenger"></button>
<span id="aria-arch-pageinfo" style="color:#8888AA;font-size:11px;min-width:140px;text-align:center;">Seite ? / ?</span>
<button class="btn secondary" onclick="loadAriaArchivePage(_ariaArchivePage+1)" id="aria-arch-next" style="padding:3px 8px;font-size:11px;" title="Eine Seite aelter"></button>
<button class="btn secondary" onclick="loadAriaArchivePage(_ariaArchivePagesTotal)" id="aria-arch-last" style="padding:3px 8px;font-size:11px;" title="Aelteste Seite">»</button>
<span style="flex:1;"></span>
<span style="color:#555570;font-size:10px;">Seite 1 = neueste · höhere Pages = älter</span>
</div>
<div id="aria-archive-list" style="flex:1;overflow-y:auto;background:#040408;font-family:'Courier New',monospace;font-size:11px;line-height:1.4;color:#C0C0D0;padding:6px 12px;">
<div style="color:#555570;font-style:italic;padding:20px;text-align:center;">Lade...</div>
</div>
</div>
</div>
<!-- Sessions + alter Brain-Viewer entfernt — Memories laufen jetzt <!-- Sessions + alter Brain-Viewer entfernt — Memories laufen jetzt
komplett ueber den Gehirn-Tab gegen die Vector-DB im aria-brain. --> komplett ueber den Gehirn-Tab gegen die Vector-DB im aria-brain. -->
@@ -405,6 +436,7 @@
<div id="live-aria-bar" style="display:flex;gap:6px;align-items:center;padding:4px 4px 6px;flex-shrink:0;"> <div id="live-aria-bar" style="display:flex;gap:6px;align-items:center;padding:4px 4px 6px;flex-shrink:0;">
<span id="live-aria-status" style="font-size:11px;color:#8888AA;flex:1;">Idle — warte auf ARIA-Aktivitaet</span> <span id="live-aria-status" style="font-size:11px;color:#8888AA;flex:1;">Idle — warte auf ARIA-Aktivitaet</span>
<button class="btn" onclick="clearAriaLive()" style="padding:4px 12px;font-size:11px;" title="Live-Mitschrift leeren">Leeren</button> <button class="btn" onclick="clearAriaLive()" style="padding:4px 12px;font-size:11px;" title="Live-Mitschrift leeren">Leeren</button>
<button class="btn" onclick="openAriaStreamModal()" style="padding:4px 12px;font-size:11px;" title="Komplettes Archiv durchblaettern">📜 Archiv</button>
<label style="font-size:11px;color:#8888AA;display:flex;align-items:center;gap:4px;cursor:pointer;" title="Bei jeder neuen Zeile ans Ende scrollen"> <label style="font-size:11px;color:#8888AA;display:flex;align-items:center;gap:4px;cursor:pointer;" title="Bei jeder neuen Zeile ans Ende scrollen">
<input type="checkbox" id="live-aria-autoscroll" checked style="margin:0;"> Auto-Scroll <input type="checkbox" id="live-aria-autoscroll" checked style="margin:0;"> Auto-Scroll
</label> </label>
@@ -3035,6 +3067,7 @@
document.getElementById('live-desktop').style.display = tab === 'desktop' ? 'block' : 'none'; document.getElementById('live-desktop').style.display = tab === 'desktop' ? 'block' : 'none';
document.getElementById('live-tab-aria').className = 'tab-btn' + (tab === 'aria' ? ' active' : ''); document.getElementById('live-tab-aria').className = 'tab-btn' + (tab === 'aria' ? ' active' : '');
document.getElementById('live-tab-desktop').className = 'tab-btn' + (tab === 'desktop' ? ' active' : ''); document.getElementById('live-tab-desktop').className = 'tab-btn' + (tab === 'desktop' ? ' active' : '');
if (tab === 'aria') loadAriaStreamHistory();
} }
// ── ARIA Live (read-only Mirror der Claude-Code-Session) ────── // ── ARIA Live (read-only Mirror der Claude-Code-Session) ──────
@@ -3150,6 +3183,127 @@
const el = _ariaStreamEl(); const el = _ariaStreamEl();
if (el) el.innerHTML = '<div style="color:#555570;font-style:italic;">Geleert.</div>'; if (el) el.innerHTML = '<div style="color:#555570;font-style:italic;">Geleert.</div>';
} }
// Beim ersten Tab-Oeffnen / Page-Reload: letzte 200 persistierte Events
// aus dem Diagnostic-Server holen. So sind die Live-Bash-Eintraege auch
// dann da wenn der Browser im Standby war.
let _ariaHistoryLoaded = false;
async function loadAriaStreamHistory(lines = 200) {
if (_ariaHistoryLoaded) return;
_ariaHistoryLoaded = true;
try {
const r = await fetch('/api/agent-stream?lines=' + lines);
if (!r.ok) return;
const d = await r.json();
const events = d.lines || [];
if (!events.length) return;
const el = _ariaStreamEl();
if (el) {
// Placeholder ('Sobald ARIA aktiv...') wegwerfen wenn vorhanden
const placeholder = el.querySelector('div[style*="italic"]');
if (placeholder) el.removeChild(placeholder);
}
_ariaPushLine(
`<span style="color:#444460;">━━━ ${events.length} fruehere Events (aus ${d.total || '?'} gespeicherten) ━━━</span>`,
'#444460',
);
for (const ev of events) {
try { appendAriaStreamEvent(ev); } catch {}
}
_ariaPushLine(
`<span style="color:#444460;">━━━ Ende History — Live ab hier ━━━</span>`,
'#444460',
);
_ariaMaybeScroll();
} catch (_) {}
}
// ── ARIA-Stream Archiv-Modal (Pagination) ────────────────
let _ariaArchivePage = 1;
let _ariaArchivePagesTotal = 1;
function openAriaStreamModal() {
const m = document.getElementById('aria-archive-modal');
if (!m) return;
m.style.display = 'flex';
loadAriaArchivePage(1);
}
function closeAriaStreamModal() {
const m = document.getElementById('aria-archive-modal');
if (m) m.style.display = 'none';
}
async function loadAriaArchivePage(page) {
const listEl = document.getElementById('aria-archive-list');
const infoEl = document.getElementById('aria-arch-pageinfo');
const totalEl = document.getElementById('aria-archive-total');
if (!listEl) return;
const perPage = parseInt(document.getElementById('aria-archive-perpage').value, 10) || 100;
page = Math.max(1, page || 1);
listEl.innerHTML = '<div style="color:#555570;font-style:italic;padding:20px;text-align:center;">Lade...</div>';
try {
const r = await fetch(`/api/agent-stream?page=${page}&perPage=${perPage}`);
if (!r.ok) throw new Error('HTTP ' + r.status);
const d = await r.json();
const events = d.lines || [];
_ariaArchivePage = d.page || page;
_ariaArchivePagesTotal = d.pagesTotal || 1;
if (totalEl) totalEl.textContent = `(${d.total || 0} Eintraege gesamt)`;
if (infoEl) infoEl.textContent = `Seite ${_ariaArchivePage} / ${_ariaArchivePagesTotal}`;
// Nav-Buttons enablen/disablen
document.getElementById('aria-arch-first').disabled = (_ariaArchivePage <= 1);
document.getElementById('aria-arch-prev').disabled = (_ariaArchivePage <= 1);
document.getElementById('aria-arch-next').disabled = (_ariaArchivePage >= _ariaArchivePagesTotal);
document.getElementById('aria-arch-last').disabled = (_ariaArchivePage >= _ariaArchivePagesTotal);
if (!events.length) {
listEl.innerHTML = '<div style="color:#555570;font-style:italic;padding:20px;text-align:center;">Keine Eintraege auf dieser Seite.</div>';
return;
}
// Eintraege rendern — wir teilen sie in HTML-Snippets analog zu
// appendAriaStreamEvent, schreiben aber direkt in den Modal-Container.
const html = events.map(p => renderArchiveLine(p)).join('');
listEl.innerHTML = html;
listEl.scrollTop = listEl.scrollHeight;
} catch (e) {
listEl.innerHTML = `<div style="color:#FF6B6B;padding:20px;">Fehler beim Laden: ${_ariaEsc(e.message)}</div>`;
}
}
function renderArchiveLine(p) {
const t = _ariaTimePrefix(p.ts);
const kind = p.kind || '';
const time = `<span style="color:#777799;">[${t}]</span>`;
if (kind === 'start') {
return `<div style="color:#444460;">━━━ ${t} session start (${_ariaEsc(p.model||'unknown')}) ━━━</div>`;
}
if (kind === 'end') {
const reason = p.reason || '?';
const codePart = (p.code != null) ? ` code=${_ariaEsc(p.code)}` : '';
const errPart = p.error ? ` err=${_ariaEsc(String(p.error).slice(0,120))}` : '';
return `<div style="color:#444460;">━━━ ${t} session end (${_ariaEsc(reason)}${codePart}${errPart}) ━━━</div>`;
}
if (kind === 'text') {
return `<div style="color:#D0D0E0;white-space:pre-wrap;word-break:break-word;">${time} ${_ariaEsc(p.text || '')}</div>`;
}
if (kind === 'thinking') {
return `<div style="color:#888866;font-style:italic;white-space:pre-wrap;word-break:break-word;">${time} 💭 ${_ariaEsc(p.text || '')}</div>`;
}
if (kind === 'tool_use') {
const name = _ariaEsc(p.name || '?');
const inp = _ariaEsc(p.input || '');
const tail = p.inputTruncatedBytes ? `<span style="color:#777799;"> ...(+${p.inputTruncatedBytes} bytes)</span>` : '';
return `<div style="color:#C0C0D0;white-space:pre-wrap;word-break:break-word;">${time} <span style="color:#0096FF;">▶ ${name}</span> <span style="color:#8888AA;">${inp}${tail}</span></div>`;
}
if (kind === 'tool_result') {
const isError = p.isError === true;
const head = isError ? '<span style="color:#FF6B6B;">✗ result (ERROR)</span>' : '<span style="color:#34C759;">✓ result</span>';
const tail = p.truncatedBytes ? `<span style="color:#777799;"> ...(+${p.truncatedBytes} bytes)</span>` : '';
return `<div style="color:#9090A0;">${time} ${head}<div style="white-space:pre-wrap;padding-left:14px;border-left:2px solid #2A2A3E;margin-top:2px;">${_ariaEsc(p.content || '')}${tail}</div></div>`;
}
return `<div style="color:#AAAACC;">${time} <span>${_ariaEsc(kind)}: ${_ariaEsc(JSON.stringify(p).slice(0, 500))}</span></div>`;
}
function ariaPanicStop() { function ariaPanicStop() {
if (!confirm('Wirklich NOT-AUS? Alle aktiven Claude-Subprocesses werden sofort gekillt.')) return; if (!confirm('Wirklich NOT-AUS? Alle aktiven Claude-Subprocesses werden sofort gekillt.')) return;
send({ action: 'aria_panic_stop' }); send({ action: 'aria_panic_stop' });
@@ -5454,6 +5608,9 @@
loadThoughtStream(); loadThoughtStream();
connectWS(); connectWS();
// ARIA-Live ist beim Page-Load schon der aktive Sub-Tab.
// History gleich nach Seitenstart laden damit Browser-Reload nichts verliert.
loadAriaStreamHistory();
</script> </script>
</body> </body>
</html> </html>
+105 -1
View File
@@ -29,6 +29,40 @@ const RVS_TLS_FALLBACK = process.env.RVS_TLS_FALLBACK || "true";
const RVS_TOKEN = process.env.RVS_TOKEN || ""; const RVS_TOKEN = process.env.RVS_TOKEN || "";
const PROXY_URL = process.env.PROXY_URL || "http://proxy:3456"; const PROXY_URL = process.env.PROXY_URL || "http://proxy:3456";
// ── Persistenz fuer agent_stream-Events ──────────────────
// Jeder agent_stream-Event wird parallel zum Broadcast in eine .jsonl
// geschrieben. Live-View laedt beim Tab-Oeffnen die letzten ~200 Zeilen,
// damit Browser-Reload / Standby den Verlauf nicht wegwerfen. Rotation
// haendelt logrotate / manual cleanup — wir cappen hier nur weichweich.
const AGENT_STREAM_LOG = process.env.AGENT_STREAM_LOG || "/shared/logs/agent_stream.jsonl";
const AGENT_STREAM_MAX_BYTES = 50 * 1024 * 1024; // 50 MB → halten den File handlebar
function appendAgentStream(payload) {
if (!payload || typeof payload !== "object") return;
try {
const line = JSON.stringify({ ts: Date.now(), ...payload }) + "\n";
// Soft-Cap: bei >50 MB ein Truncate auf den letzten ~25 MB Inhalt
try {
const st = fs.statSync(AGENT_STREAM_LOG);
if (st.size > AGENT_STREAM_MAX_BYTES) {
const half = Math.floor(AGENT_STREAM_MAX_BYTES / 2);
const fd = fs.openSync(AGENT_STREAM_LOG, "r");
const buf = Buffer.alloc(half);
fs.readSync(fd, buf, 0, half, st.size - half);
fs.closeSync(fd);
// bis zum naechsten Newline springen damit wir keine halbe Zeile haben
const firstNl = buf.indexOf(0x0a);
const start = firstNl >= 0 ? firstNl + 1 : 0;
fs.writeFileSync(AGENT_STREAM_LOG, buf.slice(start));
}
} catch {}
// Verzeichnis sicherstellen
try { fs.mkdirSync(path.dirname(AGENT_STREAM_LOG), { recursive: true }); } catch {}
fs.appendFileSync(AGENT_STREAM_LOG, line);
} catch (e) {
// Schweigend ignorieren — Persistence darf den Stream nicht blockieren
}
}
// ── State ─────────────────────────────────────────────── // ── State ───────────────────────────────────────────────
const state = { const state = {
gateway: { status: "disconnected", lastError: null, handshakeOk: false }, gateway: { status: "disconnected", lastError: null, handshakeOk: false },
@@ -637,6 +671,9 @@ function connectRVS(forcePlain) {
// Voller Live-Stream der Claude-Code-Session (assistant_text + // Voller Live-Stream der Claude-Code-Session (assistant_text +
// tool_use mit Input + tool_result mit truncated Output). Geht // tool_use mit Input + tool_result mit truncated Output). Geht
// 1:1 an Browser durch — die ARIA-Live-View rendert's. // 1:1 an Browser durch — die ARIA-Live-View rendert's.
// Zusaetzlich persistieren damit Browser-Reload / Standby den
// History-Verlauf nicht wegwirft.
try { appendAgentStream(msg.payload); } catch {}
broadcast({ type: "agent_stream", payload: msg.payload }); broadcast({ type: "agent_stream", payload: msg.payload });
} else if (msg.type === "memory_saved") { } else if (msg.type === "memory_saved") {
// ARIA hat selber etwas in die Qdrant-DB gespeichert (via memory_save Tool). // ARIA hat selber etwas in die Qdrant-DB gespeichert (via memory_save Tool).
@@ -1469,7 +1506,12 @@ const server = http.createServer((req, res) => {
log("error", "server", `zip exit ${code}: ${stderr.slice(0, 200)}`); log("error", "server", `zip exit ${code}: ${stderr.slice(0, 200)}`);
} }
}); });
req.on("close", () => { if (!zip.killed) zip.kill("SIGTERM"); }); // SIGTERM an zip nur wenn der Client wirklich disconnected
// (res.close vor res.end). req.on("close") feuert auch wenn
// der Request-Body durch ist — das wuerde zip vorzeitig killen.
res.on("close", () => {
if (!res.writableEnded && !zip.killed) zip.kill("SIGTERM");
});
}); });
return; return;
} else if (req.url === "/api/files-delete-batch" && req.method === "POST") { } else if (req.url === "/api/files-delete-batch" && req.method === "POST") {
@@ -1714,6 +1756,68 @@ const server = http.createServer((req, res) => {
}); });
req.pipe(proxyReq); req.pipe(proxyReq);
return; return;
} else if (req.url.startsWith("/api/chat-backup") && req.method === "GET") {
// Tail des chat_backup.jsonl — fuer Debug-Sessions (was hat ARIA wirklich
// gesagt/getan). ?lines=N (Default 200, Max 5000).
try {
const u = new URL(req.url, "http://localhost");
const lines = Math.max(1, Math.min(5000, parseInt(u.searchParams.get("lines") || "200", 10) || 200));
const file = "/shared/config/chat_backup.jsonl";
let raw = "";
try { raw = fs.readFileSync(file, "utf-8"); } catch {
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true, file, lines: [] }));
}
const all = raw.split("\n").filter(l => l.trim());
const tail = all.slice(-lines);
const parsed = tail.map(l => { try { return JSON.parse(l); } catch { return { _raw: l }; } });
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true, file, count: parsed.length, total: all.length, lines: parsed }));
} catch (e) {
res.writeHead(500, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: false, error: e.message }));
}
} else if (req.url.startsWith("/api/agent-stream") && req.method === "GET") {
// Tail / paginierter Slice des persistierten agent_stream.jsonl.
// Modi:
// ?lines=N → letzte N Zeilen (Live-View Initial-Load)
// ?page=P&perPage=M → 1-indexed Pagination (Modal-Browser);
// page=1 = neueste Seite, hoehere Pages = aelter
try {
const u = new URL(req.url, "http://localhost");
const linesParam = u.searchParams.get("lines");
const pageParam = u.searchParams.get("page");
const perPageParam = u.searchParams.get("perPage");
const file = AGENT_STREAM_LOG;
let raw = "";
try { raw = fs.readFileSync(file, "utf-8"); } catch {
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true, file, total: 0, lines: [] }));
}
const all = raw.split("\n").filter(l => l.trim());
let slice, page = 1, perPage = 0, pagesTotal = 1;
if (pageParam || perPageParam) {
perPage = Math.max(10, Math.min(5000, parseInt(perPageParam || "100", 10) || 100));
pagesTotal = Math.max(1, Math.ceil(all.length / perPage));
page = Math.max(1, Math.min(pagesTotal, parseInt(pageParam || "1", 10) || 1));
// page=1 = juengste Seite → vom Ende her slicen
const end = all.length - (page - 1) * perPage;
const start = Math.max(0, end - perPage);
slice = all.slice(start, end);
} else {
const lines = Math.max(1, Math.min(5000, parseInt(linesParam || "200", 10) || 200));
slice = all.slice(-lines);
}
const parsed = slice.map(l => { try { return JSON.parse(l); } catch { return { _raw: l }; } });
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({
ok: true, file, total: all.length, count: parsed.length,
page, perPage, pagesTotal, lines: parsed,
}));
} catch (e) {
res.writeHead(500, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: false, error: e.message }));
}
} else if (req.url === "/api/brain-export" && req.method === "GET") { } else if (req.url === "/api/brain-export" && req.method === "GET") {
// Komplettes Gehirn als tar.gz streamen. // Komplettes Gehirn als tar.gz streamen.
// Schritte: Brain + Qdrant stoppen (saubere Bytes) → tar streamen → wieder starten. // Schritte: Brain + Qdrant stoppen (saubere Bytes) → tar streamen → wieder starten.
+4 -7
View File
@@ -20,7 +20,7 @@ services:
volumes: volumes:
- ~/.claude:/root/.claude # Claude CLI Auth (Credentials in /root/.claude/.credentials.json) - ~/.claude:/root/.claude # Claude CLI Auth (Credentials in /root/.claude/.credentials.json)
- ./aria-data/ssh:/root/.ssh # SSH Keys fuer VM-Zugriff (aria-wohnung, rw fuer ARIA) - ./aria-data/ssh:/root/.ssh # SSH Keys fuer VM-Zugriff (aria-wohnung, rw fuer ARIA)
- aria-shared:/shared # Shared Volume fuer Datei-Austausch (Uploads von App) - ./aria-shared:/shared # Shared Volume fuer Datei-Austausch (Uploads von App)
- ./proxy-patches:/proxy-patches:ro # Tool-Use-Adapter (ueberschreibt npm-Version, read-only) - ./proxy-patches:/proxy-patches:ro # Tool-Use-Adapter (ueberschreibt npm-Version, read-only)
# Claude Code's eingebautes Auto-Memory liegt in ~/.claude/projects/. # Claude Code's eingebautes Auto-Memory liegt in ~/.claude/projects/.
# Wir ueberlagern das mit tmpfs damit ARIA nicht parallel zu ARIAs eigener # Wir ueberlagern das mit tmpfs damit ARIA nicht parallel zu ARIAs eigener
@@ -87,7 +87,7 @@ services:
- ./aria-data/brain/data:/data # Memory-Cache + Skills + Models (bind-mount fuer Export) - ./aria-data/brain/data:/data # Memory-Cache + Skills + Models (bind-mount fuer Export)
- ./aria-data/brain-import:/import:ro # Quell-MDs fuer den initialen Memory-Import (read-only) - ./aria-data/brain-import:/import:ro # Quell-MDs fuer den initialen Memory-Import (read-only)
- ./aria-data/ssh:/root/.ssh # SSH-Keys fuer aria-wohnung (geteilt mit Proxy) - ./aria-data/ssh:/root/.ssh # SSH-Keys fuer aria-wohnung (geteilt mit Proxy)
- aria-shared:/shared # gleicher Austausch-Speicher wie Bridge - ./aria-shared:/shared # gleicher Austausch-Speicher wie Bridge
restart: unless-stopped restart: unless-stopped
networks: networks:
- aria-net - aria-net
@@ -103,7 +103,7 @@ services:
ports: ports:
- "3001:3001" # Diagnostic Web-UI (Diagnostic teilt Netzwerk mit Bridge) - "3001:3001" # Diagnostic Web-UI (Diagnostic teilt Netzwerk mit Bridge)
volumes: volumes:
- aria-shared:/shared # Shared Volume fuer Datei-Austausch - ./aria-shared:/shared # Shared Volume fuer Datei-Austausch
# Audio-Zugriff # Audio-Zugriff
- /run/user/1000/pulse:/run/user/1000/pulse - /run/user/1000/pulse:/run/user/1000/pulse
- /dev/snd:/dev/snd - /dev/snd:/dev/snd
@@ -132,7 +132,7 @@ services:
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock # Container Restart + Brain-Export/Import - /var/run/docker.sock:/var/run/docker.sock # Container Restart + Brain-Export/Import
- ./aria-data/config/diag-state:/data # Persistenter State (aktive Session etc.) - ./aria-data/config/diag-state:/data # Persistenter State (aktive Session etc.)
- aria-shared:/shared # Shared Volume (Uploads + Config + Voices) - ./aria-shared:/shared # Shared Volume (Uploads + Config + Voices)
- ./aria-data/brain:/brain # Brain-Export/Import (tar.gz aus Bind-Mount) - ./aria-data/brain:/brain # Brain-Export/Import (tar.gz aus Bind-Mount)
environment: environment:
- ARIA_AUTH_TOKEN=${ARIA_AUTH_TOKEN:-} - ARIA_AUTH_TOKEN=${ARIA_AUTH_TOKEN:-}
@@ -145,9 +145,6 @@ services:
- RVS_TOKEN=${RVS_TOKEN:-} - RVS_TOKEN=${RVS_TOKEN:-}
restart: unless-stopped restart: unless-stopped
volumes:
aria-shared: # Datei-Austausch zwischen Bridge / Brain / Diagnostic
networks: networks:
aria-net: aria-net:
driver: bridge driver: bridge
+6
View File
@@ -912,6 +912,12 @@ async def run_loop(runner: F5Runner) -> None:
continue continue
await asyncio.sleep(min(retry_s, 30)) await asyncio.sleep(min(retry_s, 30))
retry_s = min(retry_s * 2, 30) retry_s = min(retry_s * 2, 30)
# Sticky-Fallback verhindern: nach jedem Disconnect-Cycle wieder
# mit wss anfangen. Sonst klebt der Client nach einem temporaeren
# TLS-Hick auf ws:// fest und kommt nie mehr auf wss zurueck —
# genau das Problem das die App + Bridge frueher schon hatten.
use_tls = RVS_TLS
tls_fallback_tried = False
async def main() -> None: async def main() -> None:
+6
View File
@@ -292,6 +292,12 @@ async def run_loop(runner: WhisperRunner) -> None:
continue continue
await asyncio.sleep(min(retry_s, 30)) await asyncio.sleep(min(retry_s, 30))
retry_s = min(retry_s * 2, 30) retry_s = min(retry_s * 2, 30)
# Sticky-Fallback verhindern: nach jedem Disconnect-Cycle wieder
# mit wss anfangen. Sonst klebt der Client nach einem temporaeren
# TLS-Hick auf ws:// fest und kommt nie mehr auf wss zurueck —
# genau das Problem das die App + Bridge frueher schon hatten.
use_tls = RVS_TLS
tls_fallback_tried = False
async def main() -> None: async def main() -> None: