feat(brain): Skills holen OAuth-Tokens vom Brain + Anti-Friedhof-Check

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 23:04:22 +02:00
parent 474e2c6c50
commit 32302a841e
2 changed files with 64 additions and 0 deletions
+44
View File
@@ -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":