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:
@@ -939,6 +939,26 @@ async def oauth_revoke_endpoint(service: str):
|
|||||||
return {"ok": oauth_mod.revoke(service)}
|
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):
|
class OAuthAuthorizeIn(BaseModel):
|
||||||
service: str
|
service: str
|
||||||
scopes: Optional[List[str]] = None
|
scopes: Optional[List[str]] = None
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ SHARED_UPLOADS = Path("/shared/uploads")
|
|||||||
|
|
||||||
VALID_EXECUTIONS = {"local-venv", "local-bin", "bash"}
|
VALID_EXECUTIONS = {"local-venv", "local-bin", "bash"}
|
||||||
NAME_RE = re.compile(r"^[a-zA-Z0-9_-]{2,60}$")
|
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:
|
def _now() -> str:
|
||||||
@@ -66,6 +68,44 @@ def _skill_dir(name: str) -> Path:
|
|||||||
return SKILLS_DIR / _safe_name(name)
|
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 ────────────────────────────────────────────────────────
|
# ─── Listing ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def list_skills(active_only: bool = False) -> list[dict]:
|
def list_skills(active_only: bool = False) -> list[dict]:
|
||||||
@@ -128,6 +168,7 @@ def create_skill(
|
|||||||
name = _safe_name(name)
|
name = _safe_name(name)
|
||||||
if execution not in VALID_EXECUTIONS:
|
if execution not in VALID_EXECUTIONS:
|
||||||
raise ValueError(f"execution muss eines von {VALID_EXECUTIONS} sein")
|
raise ValueError(f"execution muss eines von {VALID_EXECUTIONS} sein")
|
||||||
|
_check_anti_graveyard(name)
|
||||||
d = _skill_dir(name)
|
d = _skill_dir(name)
|
||||||
if d.exists():
|
if d.exists():
|
||||||
raise ValueError(f"Skill '{name}' existiert bereits — erst loeschen oder updaten")
|
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[f"ARG_{k.upper()}"] = str(v)
|
||||||
env["SKILL_DIR"] = str(d)
|
env["SKILL_DIR"] = str(d)
|
||||||
env["SHARED_UPLOADS"] = str(SHARED_UPLOADS)
|
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
|
# Command bauen
|
||||||
if exec_mode == "local-venv":
|
if exec_mode == "local-venv":
|
||||||
|
|||||||
Reference in New Issue
Block a user