mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-22 12:42:22 +02:00
112 lines
4.6 KiB
Python
112 lines
4.6 KiB
Python
"""Drop a subscription provider's 9Router lane and report only what actually happened.
|
|
|
|
Deleting is the easy half; honesty is the hard half. 9Router answers a bad id with a 404 and a JSON
|
|
error body, so a caller that ignores status codes reports "disconnected" for a lane that is still
|
|
live, and the user reconnects on top of a stale row. So success here is verified, never inferred:
|
|
`ok` means the provider has no connection row left, confirmed by re-reading 9Router.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Dict, List, Optional
|
|
|
|
import httpx
|
|
from pydantic import BaseModel, ConfigDict
|
|
from typeguard import typechecked
|
|
|
|
from backend.apps.nine_router.process import NINE_ROUTER_API, cli_auth_headers, is_running
|
|
from backend.apps.nine_router.subscription_health import invalidate_health_cache
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# gemini-cli and antigravity are two Google OAuth lanes; the registry prefers AG, so dropping gemini-cli must drop the stale AG row too or it 400s. One-directional: AG operations MUST NOT cascade back.
|
|
PROVIDER_CASCADE_REMOVES: Dict[str, List[str]] = {
|
|
"gemini-cli": ["antigravity"],
|
|
}
|
|
|
|
P_TIMEOUT_S = 10.0
|
|
|
|
|
|
class SubscriptionDisconnectResult(BaseModel):
|
|
"""What the user may be told: `ok` only once the lane is verifiably empty."""
|
|
|
|
model_config = ConfigDict(validate_assignment=True)
|
|
ok: bool
|
|
removed: int = 0
|
|
error: str = ""
|
|
|
|
|
|
@typechecked
|
|
def sync_settings_state() -> None:
|
|
"""Push the settings snapshot to the cloud state sync, exactly as connecting does. Imported late: service.client reaches back into this package."""
|
|
from backend.apps.service.client import sync
|
|
from backend.apps.settings.settings import load_settings
|
|
sync(load_settings().model_dump())
|
|
|
|
|
|
@typechecked
|
|
async def p_list_connections() -> Optional[List[Dict]]:
|
|
"""Every 9Router connection row, or None when the router can't be read.
|
|
|
|
Deliberately not `nine_router.get_providers`, which folds an unreachable router into an empty
|
|
list: here "I couldn't look" and "nothing is connected" must not be the same answer.
|
|
"""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=P_TIMEOUT_S, headers=cli_auth_headers()) as client:
|
|
r = await client.get(f"{NINE_ROUTER_API}/providers")
|
|
if r.status_code != 200:
|
|
return None
|
|
data = r.json()
|
|
if not isinstance(data, dict):
|
|
return None
|
|
return data.get("connections") or []
|
|
except Exception as e:
|
|
logger.debug(f"9Router provider list failed: {e}")
|
|
return None
|
|
|
|
|
|
@typechecked
|
|
async def delete_provider_connections(providers: List[str]) -> int:
|
|
"""Delete every 9Router connection belonging to `providers`; returns how many really went."""
|
|
connections = await p_list_connections()
|
|
if connections is None:
|
|
return 0
|
|
removed = 0
|
|
async with httpx.AsyncClient(timeout=P_TIMEOUT_S, headers=cli_auth_headers()) as client:
|
|
for c in connections:
|
|
if c.get("provider") not in providers or not c.get("id"):
|
|
continue
|
|
try:
|
|
r = await client.delete(f"{NINE_ROUTER_API}/providers/{c['id']}")
|
|
if r.status_code < 400:
|
|
removed += 1
|
|
else:
|
|
logger.warning(f"9Router refused to drop the {c.get('provider')} connection: HTTP {r.status_code}")
|
|
except Exception as e:
|
|
logger.warning(f"9Router delete failed for {c.get('provider')}: {e}")
|
|
return removed
|
|
|
|
|
|
@typechecked
|
|
async def disconnect_subscription(provider: str) -> SubscriptionDisconnectResult:
|
|
"""Clear `provider` (and its cascade lanes) from 9Router, confirming the lane is gone."""
|
|
if not is_running():
|
|
return SubscriptionDisconnectResult(ok=False, error="The subscription service isn't running.")
|
|
|
|
targets = [provider, *PROVIDER_CASCADE_REMOVES.get(provider, [])]
|
|
removed = await delete_provider_connections(targets)
|
|
|
|
remaining = await p_list_connections()
|
|
if remaining is None:
|
|
return SubscriptionDisconnectResult(
|
|
ok=False, removed=removed, error="Couldn't confirm the disconnect. Please try again.",
|
|
)
|
|
if any(c.get("provider") in targets for c in remaining):
|
|
return SubscriptionDisconnectResult(
|
|
ok=False, removed=removed, error="The subscription service kept the connection. Please try again.",
|
|
)
|
|
|
|
sync_settings_state()
|
|
# A deliberate disconnect makes the cached boot verdict a lie: it would nag "reconnect" about the lane the user just dropped.
|
|
invalidate_health_cache()
|
|
return SubscriptionDisconnectResult(ok=True, removed=removed)
|