Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0756baa2a0 | |||
| 27c9b1af96 | |||
| 70f4ff480e | |||
| c23daf14e3 | |||
| ebfde4cd1f | |||
| 5d3e3e5e8c | |||
| 0d69e211cb | |||
| 4ea13afe60 | |||
| d12bfd0302 | |||
| 8d5991f364 | |||
| 7d16a0f3e5 | |||
| 0a859f637b | |||
| 8c1476c2ca | |||
| 7d8c411f5c | |||
| fef2a32c50 | |||
| e7fd918559 | |||
| bb3c7957aa | |||
| 89cafa6251 | |||
| 1ea7ab5ab1 | |||
| 15f95ed196 | |||
| 210ce62ffe | |||
| 298b2202a1 | |||
| 845a8b0020 | |||
| 0540c49c66 | |||
| add303970b | |||
| fb71048dfd | |||
| aaaf118cb7 |
@@ -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*
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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,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
@@ -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")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 {})
|
||||||
@@ -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():
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user