andito HF Staff commited on
Commit
98c4bb7
·
1 Parent(s): 2c1477e

Forward signed-in HF identity to allocator

Browse files
Files changed (3) hide show
  1. README.md +5 -1
  2. auth.py +13 -0
  3. server.py +24 -1
README.md CHANGED
@@ -97,7 +97,11 @@ Three modes, picked by env (`/api/config` tells the client which one is active):
97
  - **`LOAD_BALANCER_URL` env** — the original flow: the browser POSTs the
98
  same-origin `/api/session` proxy, the server forwards to the LB, and the
99
  browser dials the per-session compute URL the LB hands back. The LB address
100
- never reaches the browser; the Settings URL field is hidden.
 
 
 
 
101
  - **Neither** — **Settings → Speech-to-speech server URL**: paste a full
102
  `connect_url` (`wss://host/v1/realtime?...`) or a bare host like `localhost:8080`
103
  (the app adds `/v1/realtime`), and the browser connects to it directly.
 
97
  - **`LOAD_BALANCER_URL` env** — the original flow: the browser POSTs the
98
  same-origin `/api/session` proxy, the server forwards to the LB, and the
99
  browser dials the per-session compute URL the LB hands back. The LB address
100
+ never reaches the browser; the Settings URL field is hidden. When a visitor
101
+ signs in with Hugging Face, the proxy also forwards their OAuth access token
102
+ to the LB through `X-Reachy-Mini-Authorization`, matching the Reachy Mini
103
+ client. Anonymous requests carry no token, and the token is never returned to
104
+ browser JavaScript.
105
  - **Neither** — **Settings → Speech-to-speech server URL**: paste a full
106
  `connect_url` (`wss://host/v1/realtime?...`) or a bare host like `localhost:8080`
107
  (the app adds `/v1/realtime`), and the browser connects to it directly.
auth.py CHANGED
@@ -106,6 +106,19 @@ def current_user(request):
106
  return _field(current_oauth(request), "user_info")
107
 
108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  def _user_org_names(user) -> "set[str]":
110
  """The user's organisations from the OAuth userinfo, by username/name/id."""
111
  names = set()
 
106
  return _field(current_oauth(request), "user_info")
107
 
108
 
109
+ def current_access_token(request) -> "str | None":
110
+ """The signed-in user's HF OAuth access token, or None.
111
+
112
+ Keep this server-side: it is used to attribute load-balancer session
113
+ requests to the signed-in HF account and must never be returned to the
114
+ browser.
115
+ """
116
+ token = _field(current_oauth(request), "access_token")
117
+ if token is None:
118
+ return None
119
+ return str(token).strip() or None
120
+
121
+
122
  def _user_org_names(user) -> "set[str]":
123
  """The user's organisations from the OAuth userinfo, by username/name/id."""
124
  names = set()
server.py CHANGED
@@ -81,6 +81,7 @@ SERPER_URL = "https://google.serper.dev/search"
81
  # Cap results so the tool output stays small enough to feed back to the model.
82
  MAX_RESULTS = 5
83
  HERE = os.path.dirname(os.path.abspath(__file__))
 
84
 
85
  app = FastAPI(title="s2s-demo")
86
 
@@ -250,7 +251,11 @@ async def session(request: Request):
250
  url = f"{LOAD_BALANCER_URL.rstrip('/')}/session"
251
  try:
252
  async with httpx.AsyncClient(timeout=15.0) as http:
253
- lb = await http.post(url, headers={"Content-Type": "application/json"}, content="{}")
 
 
 
 
254
  except httpx.RequestError as exc:
255
  logger.warning("Load balancer unreachable: %r", exc)
256
  raise HTTPException(status_code=502, detail="Speech service unreachable.")
@@ -286,6 +291,24 @@ async def session(request: Request):
286
  return await _finalize_grant(data, keys, tier, tracked, set_cookie)
287
 
288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  @app.get("/api/queue/{queue_id}")
290
  async def queue_status(queue_id: str, request: Request):
291
  """Poll a waiting ticket: relay the position, or — when the head of the line
 
81
  # Cap results so the tool output stays small enough to feed back to the model.
82
  MAX_RESULTS = 5
83
  HERE = os.path.dirname(os.path.abspath(__file__))
84
+ LB_USER_AGENT = "hf-realtime-voice-space"
85
 
86
  app = FastAPI(title="s2s-demo")
87
 
 
251
  url = f"{LOAD_BALANCER_URL.rstrip('/')}/session"
252
  try:
253
  async with httpx.AsyncClient(timeout=15.0) as http:
254
+ lb = await http.post(
255
+ url,
256
+ headers=_load_balancer_headers(request),
257
+ content="{}",
258
+ )
259
  except httpx.RequestError as exc:
260
  logger.warning("Load balancer unreachable: %r", exc)
261
  raise HTTPException(status_code=502, detail="Speech service unreachable.")
 
291
  return await _finalize_grant(data, keys, tier, tracked, set_cookie)
292
 
293
 
294
+ def _load_balancer_headers(request: Request) -> dict[str, str]:
295
+ """Headers for the server-to-server session allocation request.
296
+
297
+ Reachy Mini uses this dedicated header for an optional HF user token. The
298
+ load balancer fingerprints it immediately and resolves the account through
299
+ whoami asynchronously, so allocation remains fast. Anonymous visitors send
300
+ no credential header.
301
+ """
302
+ headers = {
303
+ "Content-Type": "application/json",
304
+ "User-Agent": LB_USER_AGENT,
305
+ }
306
+ token = auth.current_access_token(request)
307
+ if token:
308
+ headers["X-Reachy-Mini-Authorization"] = f"Bearer {token}"
309
+ return headers
310
+
311
+
312
  @app.get("/api/queue/{queue_id}")
313
  async def queue_status(queue_id: str, request: Request):
314
  """Poll a waiting ticket: relay the position, or — when the head of the line