Compare commits
25 Commits
fb71048dfd
...
v0.1.6.4
| 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 |
@@ -37,6 +37,12 @@ aria-data/brain/qdrant/
|
||||
# Diagnostic-State (aktive Session etc.)
|
||||
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_modules/
|
||||
npm-debug.log*
|
||||
|
||||
@@ -388,7 +388,7 @@ Chat-Turn an die richtigen Patterns.
|
||||
|
||||
### Skill-Regeln (seed_rules)
|
||||
|
||||
`aria-brain/seed_rules.py` enthaelt 9 `type=rule, pinned=true,
|
||||
`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:
|
||||
@@ -401,8 +401,27 @@ jedem Chat-Turn im Hot-Memory-Block auf:
|
||||
- **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,
|
||||
|
||||
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 10603
|
||||
versionName "0.1.6.3"
|
||||
versionCode 10604
|
||||
versionName "0.1.6.4"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -361,6 +361,12 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
|
||||
writerThread = null
|
||||
val t = track
|
||||
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.release() } catch (_: Exception) {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.1.6.3",
|
||||
"version": "0.1.6.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
+121
-10
@@ -19,6 +19,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Optional
|
||||
@@ -101,14 +102,18 @@ META_TOOLS = [
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"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."},
|
||||
"entry_code": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Python-Code. Args lesen via os.environ['ARG_NAME']. "
|
||||
"Resultat per print() (stdout) zurueck. Bei Fehler: "
|
||||
"non-zero exit (sys.exit(1) o.ae.)."
|
||||
"Python-Code. Args lesen via os.environ['ARG_<UPPER_NAME>']. "
|
||||
"WICHTIG: der Präfix `ARG_` ist Pflicht (Konvention vom "
|
||||
"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"},
|
||||
@@ -144,6 +149,13 @@ META_TOOLS = [
|
||||
"mit `-v2`/`-new`/`-fixed` Suffix anzulegen. Stefan hasst Skill-"
|
||||
"Friedhoefe. Wenn Du `youtube2mp3` umbauen sollst → `skill_update` "
|
||||
"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), "
|
||||
"`readme`, `pip_packages` (bei Aenderung wird die venv automatisch "
|
||||
"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",
|
||||
"function": {
|
||||
@@ -746,10 +799,18 @@ def _skill_to_tool(s: dict) -> dict:
|
||||
}
|
||||
if a.get("required"):
|
||||
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 {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": f"run_{s['name']}",
|
||||
"name": safe_name,
|
||||
"description": s.get("description", "(ohne Beschreibung)"),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
@@ -838,6 +899,7 @@ class Agent:
|
||||
oauth_host = os.environ.get("RVS_HOST", "").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"
|
||||
|
||||
system_prompt = build_system_prompt(hot, cold, skills=all_skills,
|
||||
triggers=all_triggers,
|
||||
condition_vars=condition_vars,
|
||||
@@ -945,6 +1007,35 @@ class Agent:
|
||||
},
|
||||
})
|
||||
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":
|
||||
items = skills_mod.list_skills(active_only=False)
|
||||
if not items:
|
||||
@@ -1047,14 +1138,34 @@ class Agent:
|
||||
f"Sicherheits-Snapshot des vorherigen Stands: {res.get('safety_snapshot')}"
|
||||
)
|
||||
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)
|
||||
snippet = (res.get("stdout") or "")[:2000] or "(kein stdout)"
|
||||
err = (res.get("stderr") or "")[:500]
|
||||
# 2000 Zeichen war viel zu wenig — Spotify-JSON ist 5-15 KB,
|
||||
# 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']})"
|
||||
out = f"{marker} · {res['duration_sec']}s\nstdout:\n{snippet}"
|
||||
if err:
|
||||
out += f"\nstderr:\n{err}"
|
||||
if stderr:
|
||||
out += f"\nstderr:\n{stderr}"
|
||||
return out
|
||||
if name == "trigger_timer":
|
||||
fires_at_iso = arguments.get("fires_at")
|
||||
|
||||
@@ -798,6 +798,32 @@ def skills_get(name: str):
|
||||
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")
|
||||
def skills_create(body: SkillCreate):
|
||||
try:
|
||||
|
||||
@@ -27,6 +27,46 @@ logger = logging.getLogger(__name__)
|
||||
# Jede Regel = ein eigener Memory-Punkt. Klein halten, klar formulieren —
|
||||
# ARIA sieht das in jedem Chat-Turn als pinned Hot Memory.
|
||||
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",
|
||||
"type": "rule",
|
||||
@@ -39,6 +79,32 @@ SEED_RULES: List[dict] = [
|
||||
"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",
|
||||
"type": "rule",
|
||||
@@ -162,6 +228,355 @@ SEED_RULES: List[dict] = [
|
||||
"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",
|
||||
"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
|
||||
|
||||
|
||||
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:
|
||||
d = _skill_dir(name)
|
||||
if not d.exists():
|
||||
|
||||
@@ -1506,7 +1506,12 @@ const server = http.createServer((req, res) => {
|
||||
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;
|
||||
} else if (req.url === "/api/files-delete-batch" && req.method === "POST") {
|
||||
|
||||
+4
-7
@@ -20,7 +20,7 @@ services:
|
||||
volumes:
|
||||
- ~/.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-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)
|
||||
# Claude Code's eingebautes Auto-Memory liegt in ~/.claude/projects/.
|
||||
# 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-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-shared:/shared # gleicher Austausch-Speicher wie Bridge
|
||||
- ./aria-shared:/shared # gleicher Austausch-Speicher wie Bridge
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- aria-net
|
||||
@@ -103,7 +103,7 @@ services:
|
||||
ports:
|
||||
- "3001:3001" # Diagnostic Web-UI (Diagnostic teilt Netzwerk mit Bridge)
|
||||
volumes:
|
||||
- aria-shared:/shared # Shared Volume fuer Datei-Austausch
|
||||
- ./aria-shared:/shared # Shared Volume fuer Datei-Austausch
|
||||
# Audio-Zugriff
|
||||
- /run/user/1000/pulse:/run/user/1000/pulse
|
||||
- /dev/snd:/dev/snd
|
||||
@@ -132,7 +132,7 @@ services:
|
||||
volumes:
|
||||
- /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-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)
|
||||
environment:
|
||||
- ARIA_AUTH_TOKEN=${ARIA_AUTH_TOKEN:-}
|
||||
@@ -145,9 +145,6 @@ services:
|
||||
- RVS_TOKEN=${RVS_TOKEN:-}
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
aria-shared: # Datei-Austausch zwischen Bridge / Brain / Diagnostic
|
||||
|
||||
networks:
|
||||
aria-net:
|
||||
driver: bridge
|
||||
|
||||
@@ -912,6 +912,12 @@ async def run_loop(runner: F5Runner) -> None:
|
||||
continue
|
||||
await asyncio.sleep(min(retry_s, 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:
|
||||
|
||||
@@ -292,6 +292,12 @@ async def run_loop(runner: WhisperRunner) -> None:
|
||||
continue
|
||||
await asyncio.sleep(min(retry_s, 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:
|
||||
|
||||
Reference in New Issue
Block a user