feat(oauth): ARIA kann Provider selbst registrieren + Custom-Provider in Diagnostic & App

ARIA hat jetzt das META-Tool oauth_register_provider. Wenn Stefan einen
Service nutzen will, der nicht in den (auf Spotify reduzierten) Defaults
ist, kann sie auth_url/token_url/scopes/client_auth selbst eintragen —
ARIA kennt typische OAuth-Endpunkte (Dropbox, Discord, Notion, Slack,
Zoom, Trello, LinkedIn, Reddit, Twitch) aus ihrem Training. Sie traegt
NUR die URLs ein, client_id/secret bleiben Stefans Job (Diagnostic /
App-UI) — bewusste Trennung damit Credentials nicht im Chat-Verlauf
landen.

DEFAULT_PROVIDERS auf Spotify reduziert — Rest war aktuell ungenutzt
und macht den Code unnoetig "groß". ARIA registriert on-demand.

Diagnostic-UI:
- Custom-Provider zeigen auth_url/token_url/scopes als sichtbare Felder
- Defaults verstecken die Felder hinter "Default-URLs ueberschreiben
  (advanced)" damit man die Spotify-URLs nicht versehentlich loescht
- "+ Custom OAuth-Provider hinzufuegen" Button mit Prompts fuer
  Name/URLs/Scopes
- 🗑-Icon bei Custom-Services (Service komplett entfernen)

App-UI (neu fuer unterwegs):
- Settings → Sektion 🔑 "OAuth-Apps" zwischen Skills und Protokoll
- OAuthBrowser-Komponente analog zu Trigger/Skill-Browser:
  Liste mit Status, Tap → Edit-Modal mit client_id/secret +
  Advanced-Toggle fuer URLs. "Autorisieren ↗" oeffnet System-Browser
  via Linking.openURL, redirected zur RVS-Callback-Page,
  Status-Refresh nach 8s.
- "+ Custom"-Button → Full-Screen-Modal fuer Service-Anlage.
- brainApi um listOAuthServices/getOAuthApps/saveOAuthApp/
  deleteOAuthApp/authorizeOAuth/revokeOAuth erweitert.

Workflow ist jetzt: "verbinde mich mit Dropbox" → ARIA registriert
Provider → "trag client_id/secret in Settings ein" → Stefan macht das
in App oder Diagnostic → "Autorisieren ↗" → fertig.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 20:16:31 +02:00
parent 30c1dd7473
commit 13e87fb083
6 changed files with 916 additions and 31 deletions
+44 -28
View File
@@ -40,7 +40,10 @@ APPS_FILE = CONFIG_DIR / "oauth_apps.json"
TOKENS_FILE = CONFIG_DIR / "oauth_tokens.json"
# Default-Provider-Configs. Werden von oauth_apps.json gemergt (User-Config
# uebersteuert). Stefan muss nur client_id + client_secret eintragen.
# uebersteuert). Aktuell nur Spotify als out-of-the-box Service — fuer alles
# andere benutzt ARIA das `oauth_register_provider` Tool (legt Provider on-
# demand mit den jeweiligen Endpunkten an). Stefan muss bei jedem Provider
# danach nur client_id + client_secret in Diagnostic / App eintragen.
DEFAULT_PROVIDERS: dict[str, dict] = {
"spotify": {
"auth_url": "https://accounts.spotify.com/authorize",
@@ -50,33 +53,6 @@ DEFAULT_PROVIDERS: dict[str, dict] = {
"user-library-read"],
"client_auth": "basic", # client_id:client_secret als Basic-Auth-Header
},
"google": {
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"scopes": ["openid", "email", "profile"],
"client_auth": "body", # client_id+secret im Body
"extra_auth_params": {"access_type": "offline", "prompt": "consent"},
},
"github": {
"auth_url": "https://github.com/login/oauth/authorize",
"token_url": "https://github.com/login/oauth/access_token",
"scopes": ["read:user"],
"client_auth": "body",
"accept_header": "application/json", # GitHub returns form-urlencoded otherwise
},
"strava": {
"auth_url": "https://www.strava.com/oauth/authorize",
"token_url": "https://www.strava.com/oauth/token",
"scopes": ["read", "activity:read_all"],
"client_auth": "body",
"extra_auth_params": {"approval_prompt": "auto"},
},
"microsoft": {
"auth_url": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
"token_url": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
"scopes": ["User.Read", "offline_access"],
"client_auth": "body",
},
}
# Pending Auth-Requests: state → {service, scopes, redirect_uri, created_at}
@@ -149,6 +125,46 @@ def _provider_credentials(service: str) -> tuple[str, str]:
return cid, sec
def register_provider(service: str, auth_url: str, token_url: str,
scopes: Optional[list[str]] = None,
client_auth: str = "body",
extra_auth_params: Optional[dict] = None,
accept_header: Optional[str] = None) -> dict:
"""Schreibt einen neuen Provider-Eintrag in oauth_apps.json. KEINE
Credentials hier — die bleiben Stefans Job (Diagnostic / App-UI). Wird
vom Brain-Tool `oauth_register_provider` gerufen.
Wenn der Service schon existiert: URLs/Scopes werden ueberschrieben,
aber vorhandene client_id/client_secret bleiben unberuehrt.
"""
svc = (service or "").strip()
if not svc or not all(c.isalnum() or c in "_-" for c in svc) or len(svc) > 60:
raise ValueError(f"Ungueltiger service-Name: {service!r}")
if not auth_url.startswith(("http://", "https://")):
raise ValueError(f"auth_url muss http(s):// sein: {auth_url!r}")
if not token_url.startswith(("http://", "https://")):
raise ValueError(f"token_url muss http(s):// sein: {token_url!r}")
if client_auth not in ("body", "basic"):
raise ValueError(f"client_auth muss 'body' oder 'basic' sein: {client_auth!r}")
apps = _load_json(APPS_FILE)
entry = apps.get(svc) or {}
entry["auth_url"] = auth_url.strip()
entry["token_url"] = token_url.strip()
if scopes is not None:
entry["scopes"] = list(scopes)
entry["client_auth"] = client_auth
if extra_auth_params is not None:
entry["extra_auth_params"] = extra_auth_params
if accept_header is not None:
entry["accept_header"] = accept_header
apps[svc] = entry
_save_json(APPS_FILE, apps)
logger.info("[oauth] Provider '%s' registriert (auth=%s, token=%s, scopes=%d)",
svc, auth_url, token_url, len(entry.get("scopes") or []))
return entry
def _cleanup_pending() -> None:
"""Entfernt abgelaufene Pending-Auths."""
now = time.time()