From 32302a841ecc24daa4c204088013ff7cd15f09c8 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Thu, 28 May 2026 23:04:22 +0200 Subject: [PATCH] feat(brain): Skills holen OAuth-Tokens vom Brain + Anti-Friedhof-Check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1+P2-Infrastruktur: - Neuer Endpoint GET /oauth/{service}/token liefert aktuelles access_token mit Auto-Refresh (< 60s Restzeit). Skills rufen das ueber BRAIN_INTERNAL_URL ab statt client_secret hardzucoden. - run_skill setzt BRAIN_INTERNAL_URL als ENV (Default http://localhost:8080, override via Brain-Env). Skills laufen im Brain-Container, localhost passt. - skills.create_skill: _check_anti_graveyard rejected Versions-Suffixe (-v2, _v3, -new, -fixed, -old, -alt, -copy, -final, -clean) und Prefix-Kollisionen (z.B. spotify-aria wenn spotify schon existiert) — die zwei Patterns hinter dem alten Skill-Friedhof. Tool-Description fuer skill_create um PFLICHT-VORHER-Block ergaenzt (skill_list, kein Versionssuffix, oauth_get_token, config_schema) damit ARIA die Regeln direkt im Schema sieht. Co-Authored-By: Claude Opus 4.7 (1M context) --- aria-brain/main.py | 20 ++++++++++++++++++++ aria-brain/skills.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/aria-brain/main.py b/aria-brain/main.py index 1979210..24e197d 100644 --- a/aria-brain/main.py +++ b/aria-brain/main.py @@ -939,6 +939,26 @@ async def oauth_revoke_endpoint(service: str): return {"ok": oauth_mod.revoke(service)} +@app.get("/oauth/{service}/token") +async def oauth_token_endpoint(service: str): + """Liefert das aktuelle access_token fuer einen Service (mit Auto-Refresh + wenn < 60s Restzeit). Nur fuer interne Skill-Aufrufe gedacht — Skills + sollen NIEMALS hardcoded client_secrets haben, sondern dieses Endpoint + pollen. Antwort: {access_token, expires_at, expires_in_sec}. + Bei nicht-autorisiert: 401 mit klarer Message.""" + try: + rec = oauth_mod.get_token(service) + except RuntimeError as exc: + raise HTTPException(401, str(exc)) + expires_at = int(rec.get("expires_at") or 0) + import time as _t + return { + "access_token": rec.get("access_token"), + "expires_at": expires_at, + "expires_in_sec": max(0, expires_at - int(_t.time())), + } + + class OAuthAuthorizeIn(BaseModel): service: str scopes: Optional[List[str]] = None diff --git a/aria-brain/skills.py b/aria-brain/skills.py index 87b170f..815f48b 100644 --- a/aria-brain/skills.py +++ b/aria-brain/skills.py @@ -50,6 +50,8 @@ SHARED_UPLOADS = Path("/shared/uploads") VALID_EXECUTIONS = {"local-venv", "local-bin", "bash"} NAME_RE = re.compile(r"^[a-zA-Z0-9_-]{2,60}$") +# Anti-Skill-Friedhof: ARIAs Lieblings-Suffixe wenn sie statt updaten neu baut +VERSION_SUFFIX_RE = re.compile(r"(?:[-_]v\d+|[-_](?:new|fixed|old|alt|copy|final|clean))$", re.I) def _now() -> str: @@ -66,6 +68,44 @@ def _skill_dir(name: str) -> Path: return SKILLS_DIR / _safe_name(name) +def _check_anti_graveyard(name: str) -> None: + """Verhindert klassische Skill-Friedhof-Patterns beim Anlegen. + + Hard-Reject auf: + 1. Versions-Suffixe (`-v2`, `_v3`, `-new`, `-fixed`, …) im Namen + 2. Prefix-Kollision mit existierendem Skill (z.B. `spotify` existiert, + jemand will `spotify-aria` oder `spotify-ctl` anlegen) + """ + if VERSION_SUFFIX_RE.search(name): + raise ValueError( + f"Skill-Name '{name}' enthaelt einen Versions-Suffix " + f"(-v2 / _v3 / -new / -fixed / -old / -alt / -copy / -final / -clean). " + f"Skills werden intern versioniert (skill_rollback). " + f"Waehle einen klaren Namen ohne Suffix oder nutze skill_update auf " + f"den bestehenden Skill." + ) + if not SKILLS_DIR.exists(): + return + existing = [p.name for p in SKILLS_DIR.iterdir() if p.is_dir()] + for ex in existing: + if ex == name: + continue # wird spaeter mit "existiert bereits" abgefangen + # neuer Name verlaengert existierenden Stem: 'spotify' da, neu 'spotify-aria' + if name.startswith(ex + "-") or name.startswith(ex + "_"): + raise ValueError( + f"Skill-Name '{name}' kollidiert mit existierendem '{ex}'. " + f"Wenn Du '{ex}' verbessern willst: skill_update auf '{ex}'. " + f"Wenn es wirklich was anderes ist: waehle einen Namen ohne den " + f"Praefix '{ex}-' / '{ex}_'." + ) + # neuer Name ist Kurzform eines existierenden: 'spotify-aria' da, neu 'spotify' + if ex.startswith(name + "-") or ex.startswith(name + "_"): + raise ValueError( + f"Es existiert bereits '{ex}' mit Praefix '{name}'. Pruefe ob '{ex}' " + f"das schon kann; wenn ja: skill_update auf '{ex}' oder Skill umbenennen." + ) + + # ─── Listing ──────────────────────────────────────────────────────── def list_skills(active_only: bool = False) -> list[dict]: @@ -128,6 +168,7 @@ def create_skill( name = _safe_name(name) if execution not in VALID_EXECUTIONS: raise ValueError(f"execution muss eines von {VALID_EXECUTIONS} sein") + _check_anti_graveyard(name) d = _skill_dir(name) if d.exists(): raise ValueError(f"Skill '{name}' existiert bereits — erst loeschen oder updaten") @@ -284,6 +325,9 @@ def run_skill(name: str, args: Optional[dict] = None, timeout_sec: int = 300) -> env[f"ARG_{k.upper()}"] = str(v) env["SKILL_DIR"] = str(d) env["SHARED_UPLOADS"] = str(SHARED_UPLOADS) + # Brain-API fuer Skills die OAuth-Tokens / Brain-Helpers brauchen. + # Beispiel: requests.get(f"{os.environ['BRAIN_INTERNAL_URL']}/oauth/spotify/token") + env["BRAIN_INTERNAL_URL"] = os.environ.get("BRAIN_INTERNAL_URL", "http://localhost:8080") # Command bauen if exec_mode == "local-venv":