feat(oauth): generische OAuth2-Pipeline ueber RVS-Callback (Spotify/Google/GitHub/Strava/MS)

Bisher musste Stefan bei OAuth-Flows manuell den Auth-Code aus der
Browser-URL kopieren (redirect_uri war localhost). Jetzt: RVS hat einen
HTTP-Listener auf demselben Port wie der WebSocket, Provider redirecten
nach Auth zu https://{RVS_HOST}/oauth/callback/{service}, RVS broadcastet,
aria-bridge forwarded, Brain matched state + tauscht code gegen Token.
Token-Refresh laeuft automatisch.

- rvs/server.js: hybrid http.createServer + WebSocketServer{noServer}.
  Route GET /oauth/callback/{service}, broadcast oauth_callback an alle
  Raeume, schoene Dark-Mode-HTML-Antwort an den Browser (Auto-Close 4s).
- bridge/aria_bridge.py: empfaengt oauth_callback, POSTet an Brain
  /internal/oauth-callback.
- aria-brain/oauth.py: neuer Manager. Pending-Store mit state+TTL,
  Token-Exchange (Basic-Auth oder Body je nach Provider), persistente
  Speicherung in /shared/config/oauth_tokens.json (mode 0600),
  Token-Refresh wenn <60s Restzeit. Vordefinierte Configs fuer Spotify,
  Google, GitHub, Strava, Microsoft.
- aria-brain/agent.py: META-Tools oauth_authorize / oauth_get_token /
  oauth_revoke.
- aria-brain/prompts.py: System-Prompt-Block zeigt ARIA die feste
  Callback-URL als Quelle der Wahrheit + aktuelle Service-States.
- aria-brain/main.py: HTTP-Endpoints /oauth/services, /oauth/apps,
  /oauth/authorize, /oauth/{service}/revoke, /internal/oauth-callback.
- diagnostic: neue Section "OAuth-Apps". Pro Service Karte mit Status,
  client_id + client_secret (Passwort-Toggle), Speichern + Autorisieren-
  Buttons. Authorize oeffnet Provider-Auth in neuem Tab.
- docker-compose.yml: brain-env um RVS_HOST + RVS_PORT_PUBLIC + RVS_TLS
  ergaenzt (Brain braucht die Werte zum Bau der Callback-URL).
- .env.example: RVS_PORT_PUBLIC + Brain-Timeout-Vars (PROXY_TIMEOUT_SEC
  + Connect/Write/Pool) dokumentiert.
- README.md: OAuth-Pipeline + ARIA-Live-Mirror in Diagnostic-Section,
  OAuth-Apps in Einstellungen-Tab erwaehnt.
- issue.md: OAuth-Pipeline + Brain-Timeout-Fix als erledigt dokumentiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 15:39:54 +02:00
parent 0887674497
commit acaa9fc3f2
11 changed files with 1150 additions and 10 deletions
+33
View File
@@ -2338,6 +2338,13 @@ class ARIABridge:
future.set_result(text)
return
elif msg_type == "oauth_callback":
# RVS hat einen OAuth-Provider-Callback empfangen (z.B. Spotify
# nach User-Authorize) und broadcastet ihn. Wir forwarden an Brain,
# das den state-Match macht + code gegen access_token tauscht.
asyncio.create_task(self._forward_oauth_callback(payload))
return
elif msg_type == "flux_response":
# Antwort der flux-bridge auf unseren flux_request. Erste Nachricht
# mit state='rendering' ist nur Progress-Ping — die echte Antwort
@@ -2715,6 +2722,32 @@ class ARIABridge:
status = await asyncio.get_event_loop().run_in_executor(None, _do_request)
logger.info("[cancel] Diagnostic /api/cancel: %s", status)
async def _forward_oauth_callback(self, payload: dict) -> None:
"""Forwarded den OAuth-Callback (kommt via RVS vom RVS-HTTP-Handler)
per HTTP an Brain. Brain hat den pending-state + macht den token-
exchange. Fire-and-forget — bei Failure loggen wir nur."""
service = (payload.get("service") or "").strip()
if not service:
logger.warning("[oauth] callback ohne service, ignoriert")
return
brain_url = os.environ.get("BRAIN_URL", "http://aria-brain:8080")
url = f"{brain_url}/internal/oauth-callback"
def _do_request():
try:
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
url, data=data, method="POST",
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=10) as resp:
return resp.status, resp.read().decode("utf-8", "ignore")[:200]
except Exception as e:
return f"error: {e}", ""
status, body = await asyncio.get_event_loop().run_in_executor(None, _do_request)
logger.info("[oauth] Forward %s → brain: %s %s", service, status, body)
async def _cancel_proxy_subprocesses(self) -> None:
"""Not-Aus: ruft den proxy-internen /cancel-all Side-Channel auf
(siehe proxy-patches/routes.js). Killt alle aktiven Claude-Code-