lewtun HF Staff OpenAI Codex commited on
Commit
53d5c85
·
2 Parent(s): c69090d5de6e8f

Deploy 2026-05-04

Browse files

Co-authored-by: OpenAI Codex <codex@openai.com>

backend/dependencies.py CHANGED
@@ -12,7 +12,7 @@ from typing import Any
12
  import httpx
13
  from fastapi import HTTPException, Request, status
14
 
15
- from agent.core.hf_tokens import bearer_token_from_header
16
 
17
  from agent.core.hf_access import fetch_whoami_v2
18
 
@@ -36,6 +36,8 @@ DEV_USER: dict[str, Any] = {
36
  "plan": "org", # Dev runs at the Pro/Org quota tier so local testing isn't capped.
37
  }
38
 
 
 
39
  # Plan field discovery — log the whoami-v2 shape once at DEBUG so we can
40
  # confirm the actual key in production without hammering the HF API.
41
  _WHOAMI_SHAPE_LOGGED = False
@@ -135,6 +137,7 @@ async def _extract_user_from_token(token: str) -> dict[str, Any] | None:
135
  return None
136
  user = _user_from_info(user_info)
137
  user["plan"] = await _fetch_user_plan(token)
 
138
  return user
139
 
140
 
@@ -145,13 +148,13 @@ async def _dev_user_from_env() -> dict[str, Any]:
145
  real HF namespace. Deriving the dev user from HF_TOKEN keeps local uploads
146
  pointed at the token owner's dataset instead of dev/ml-intern-sessions.
147
  """
148
- token = os.environ.get("HF_TOKEN", "")
149
  if not token:
150
- return DEV_USER
151
 
152
  whoami = await fetch_whoami_v2(token)
153
  if not isinstance(whoami, dict):
154
- return DEV_USER
155
 
156
  username = None
157
  for key in ("name", "user", "preferred_username"):
@@ -160,13 +163,14 @@ async def _dev_user_from_env() -> dict[str, Any]:
160
  username = value
161
  break
162
  if not username:
163
- return DEV_USER
164
 
165
  return {
166
  "user_id": username,
167
  "username": username,
168
  "authenticated": True,
169
  "plan": await _fetch_user_plan(token),
 
170
  }
171
 
172
 
 
12
  import httpx
13
  from fastapi import HTTPException, Request, status
14
 
15
+ from agent.core.hf_tokens import bearer_token_from_header, clean_hf_token
16
 
17
  from agent.core.hf_access import fetch_whoami_v2
18
 
 
36
  "plan": "org", # Dev runs at the Pro/Org quota tier so local testing isn't capped.
37
  }
38
 
39
+ INTERNAL_HF_TOKEN_KEY = "_hf_token"
40
+
41
  # Plan field discovery — log the whoami-v2 shape once at DEBUG so we can
42
  # confirm the actual key in production without hammering the HF API.
43
  _WHOAMI_SHAPE_LOGGED = False
 
137
  return None
138
  user = _user_from_info(user_info)
139
  user["plan"] = await _fetch_user_plan(token)
140
+ user[INTERNAL_HF_TOKEN_KEY] = clean_hf_token(token)
141
  return user
142
 
143
 
 
148
  real HF namespace. Deriving the dev user from HF_TOKEN keeps local uploads
149
  pointed at the token owner's dataset instead of dev/ml-intern-sessions.
150
  """
151
+ token = clean_hf_token(os.environ.get("HF_TOKEN", ""))
152
  if not token:
153
+ return dict(DEV_USER)
154
 
155
  whoami = await fetch_whoami_v2(token)
156
  if not isinstance(whoami, dict):
157
+ return dict(DEV_USER)
158
 
159
  username = None
160
  for key in ("name", "user", "preferred_username"):
 
163
  username = value
164
  break
165
  if not username:
166
+ return dict(DEV_USER)
167
 
168
  return {
169
  "user_id": username,
170
  "username": username,
171
  "authenticated": True,
172
  "plan": await _fetch_user_plan(token),
173
+ INTERNAL_HF_TOKEN_KEY: token,
174
  }
175
 
176
 
backend/routes/agent.py CHANGED
@@ -10,7 +10,11 @@ import logging
10
  import os
11
  from typing import Any
12
 
13
- from dependencies import get_current_user, require_huggingface_org_member
 
 
 
 
14
  from fastapi import (
15
  APIRouter,
16
  Depends,
@@ -209,6 +213,12 @@ async def _enforce_gated_model_quota(
209
  await session_manager.persist_session_snapshot(agent_session)
210
 
211
 
 
 
 
 
 
 
212
  async def _check_session_access(
213
  session_id: str,
214
  user: dict[str, Any],
@@ -216,7 +226,7 @@ async def _check_session_access(
216
  preload_sandbox: bool = True,
217
  ) -> AgentSession:
218
  """Verify and lazily load the user's session. Raises 403 or 404."""
219
- hf_token = resolve_hf_request_token(request) if request is not None else user.get("hf_token")
220
  agent_session = await session_manager.ensure_session_loaded(
221
  session_id,
222
  user["user_id"],
@@ -318,9 +328,7 @@ async def generate_title(
318
  reasoning model — reasoning_effort=low keeps the reasoning budget small
319
  so the 60-token output budget isn't consumed before the title is written.
320
  """
321
- api_key = resolve_hf_router_token(
322
- user.get("hf_token") if isinstance(user, dict) else None
323
- )
324
  try:
325
  response = await acompletion(
326
  # Double openai/ prefix: LiteLLM strips the first as its provider
 
10
  import os
11
  from typing import Any
12
 
13
+ from dependencies import (
14
+ INTERNAL_HF_TOKEN_KEY,
15
+ get_current_user,
16
+ require_huggingface_org_member,
17
+ )
18
  from fastapi import (
19
  APIRouter,
20
  Depends,
 
213
  await session_manager.persist_session_snapshot(agent_session)
214
 
215
 
216
+ def _user_hf_token(user: dict[str, Any] | None) -> str | None:
217
+ if not isinstance(user, dict):
218
+ return None
219
+ return user.get(INTERNAL_HF_TOKEN_KEY)
220
+
221
+
222
  async def _check_session_access(
223
  session_id: str,
224
  user: dict[str, Any],
 
226
  preload_sandbox: bool = True,
227
  ) -> AgentSession:
228
  """Verify and lazily load the user's session. Raises 403 or 404."""
229
+ hf_token = resolve_hf_request_token(request) if request is not None else _user_hf_token(user)
230
  agent_session = await session_manager.ensure_session_loaded(
231
  session_id,
232
  user["user_id"],
 
328
  reasoning model — reasoning_effort=low keeps the reasoning budget small
329
  so the 60-token output budget isn't consumed before the title is written.
330
  """
331
+ api_key = resolve_hf_router_token(_user_hf_token(user))
 
 
332
  try:
333
  response = await acompletion(
334
  # Double openai/ prefix: LiteLLM strips the first as its provider
backend/routes/auth.py CHANGED
@@ -167,6 +167,5 @@ async def get_me(user: dict = Depends(get_current_user)) -> dict:
167
 
168
  Uses the shared auth dependency which handles cookie + Bearer token.
169
  """
170
- return user
171
-
172
 
 
167
 
168
  Uses the shared auth dependency which handles cookie + Bearer token.
169
  """
170
+ return {key: value for key, value in user.items() if not key.startswith("_")}
 
171
 
backend/session_manager.py CHANGED
@@ -376,6 +376,40 @@ class SessionManager:
376
  agent_session.hf_username = hf_username
377
  agent_session.session.hf_username = hf_username
378
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  async def _clear_persisted_sandbox_metadata(self, session_id: str) -> None:
380
  try:
381
  await self._store().update_session_fields(
@@ -526,6 +560,10 @@ class SessionManager:
526
  hf_token=hf_token,
527
  hf_username=hf_username,
528
  )
 
 
 
 
529
  return existing
530
  return None
531
 
@@ -543,6 +581,10 @@ class SessionManager:
543
  hf_token=hf_token,
544
  hf_username=hf_username,
545
  )
 
 
 
 
546
  return existing
547
  return None
548
 
 
376
  agent_session.hf_username = hf_username
377
  agent_session.session.hf_username = hf_username
378
 
379
+ @staticmethod
380
+ def _has_active_sandbox_preload(agent_session: AgentSession) -> bool:
381
+ task = getattr(agent_session.session, "sandbox_preload_task", None)
382
+ return bool(task and not task.done())
383
+
384
+ @staticmethod
385
+ def _preload_failed_for_missing_hf_token(agent_session: AgentSession) -> bool:
386
+ error = getattr(agent_session.session, "sandbox_preload_error", None)
387
+ return isinstance(error, str) and error.startswith("No HF token available")
388
+
389
+ def _restart_cpu_preload_if_token_recovered(
390
+ self,
391
+ agent_session: AgentSession,
392
+ *,
393
+ preload_sandbox: bool,
394
+ ) -> None:
395
+ if not preload_sandbox:
396
+ return
397
+ session = agent_session.session
398
+ if getattr(session, "sandbox", None):
399
+ return
400
+ if self._has_active_sandbox_preload(agent_session):
401
+ return
402
+ if not (agent_session.hf_token or getattr(session, "hf_token", None)):
403
+ return
404
+
405
+ if not self._preload_failed_for_missing_hf_token(agent_session):
406
+ return
407
+
408
+ session.sandbox_preload_error = None
409
+ session.sandbox_preload_task = None
410
+ session.sandbox_preload_cancel_event = None
411
+ self._start_cpu_sandbox_preload(agent_session)
412
+
413
  async def _clear_persisted_sandbox_metadata(self, session_id: str) -> None:
414
  try:
415
  await self._store().update_session_fields(
 
560
  hf_token=hf_token,
561
  hf_username=hf_username,
562
  )
563
+ self._restart_cpu_preload_if_token_recovered(
564
+ existing,
565
+ preload_sandbox=preload_sandbox,
566
+ )
567
  return existing
568
  return None
569
 
 
581
  hf_token=hf_token,
582
  hf_username=hf_username,
583
  )
584
+ self._restart_cpu_preload_if_token_recovered(
585
+ existing,
586
+ preload_sandbox=preload_sandbox,
587
+ )
588
  return existing
589
  return None
590
 
tests/unit/test_auth_token_propagation.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for authenticated HF token propagation through backend dependencies."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from types import SimpleNamespace
6
+
7
+ import pytest
8
+
9
+ _BACKEND_DIR = Path(__file__).resolve().parent.parent.parent / "backend"
10
+ if str(_BACKEND_DIR) not in sys.path:
11
+ sys.path.insert(0, str(_BACKEND_DIR))
12
+
13
+ import dependencies # noqa: E402
14
+ from routes import auth # noqa: E402
15
+
16
+
17
+ @pytest.mark.asyncio
18
+ async def test_current_user_carries_internal_hf_token(monkeypatch):
19
+ monkeypatch.setattr(dependencies, "AUTH_ENABLED", True)
20
+ dependencies._token_cache.clear()
21
+
22
+ async def fake_validate_token(token):
23
+ assert token == "hf-user-token"
24
+ return {"sub": "user-id", "preferred_username": "alice"}
25
+
26
+ async def fake_fetch_user_plan(token):
27
+ assert token == "hf-user-token"
28
+ return "pro"
29
+
30
+ monkeypatch.setattr(dependencies, "_validate_token", fake_validate_token)
31
+ monkeypatch.setattr(dependencies, "_fetch_user_plan", fake_fetch_user_plan)
32
+
33
+ request = SimpleNamespace(
34
+ headers={"Authorization": "Bearer hf-user-token"},
35
+ cookies={},
36
+ )
37
+
38
+ user = await dependencies.get_current_user(request)
39
+
40
+ assert user["user_id"] == "user-id"
41
+ assert user["username"] == "alice"
42
+ assert user["plan"] == "pro"
43
+ assert user[dependencies.INTERNAL_HF_TOKEN_KEY] == "hf-user-token"
44
+
45
+
46
+ @pytest.mark.asyncio
47
+ async def test_auth_me_does_not_expose_internal_hf_token():
48
+ user = {
49
+ "user_id": "user-id",
50
+ "username": "alice",
51
+ "authenticated": True,
52
+ dependencies.INTERNAL_HF_TOKEN_KEY: "hf-user-token",
53
+ }
54
+
55
+ response = await auth.get_me(user)
56
+
57
+ assert response == {
58
+ "user_id": "user-id",
59
+ "username": "alice",
60
+ "authenticated": True,
61
+ }
tests/unit/test_session_manager_persistence.py CHANGED
@@ -281,6 +281,101 @@ async def test_existing_session_updates_token_after_access_check():
281
  assert existing.session.hf_token == "new-token"
282
 
283
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  @pytest.mark.asyncio
285
  async def test_concurrent_lazy_restore_starts_only_one_agent_task():
286
  store = RestoreStore(delay=0.01)
 
281
  assert existing.session.hf_token == "new-token"
282
 
283
 
284
+ @pytest.mark.asyncio
285
+ async def test_existing_session_retries_preload_after_token_recovered():
286
+ manager = _manager_with_store(NoopSessionStore())
287
+ existing = _runtime_agent_session("s1", user_id="owner", hf_token=None)
288
+ done_task = asyncio.get_running_loop().create_future()
289
+ done_task.set_result(None)
290
+ existing.session.sandbox_preload_task = done_task
291
+ existing.session.sandbox_preload_error = (
292
+ "No HF token available. Cannot create sandbox."
293
+ )
294
+ manager.sessions["s1"] = existing
295
+ started: list[str] = []
296
+
297
+ def fake_start_cpu_sandbox_preload(agent_session):
298
+ started.append(agent_session.session_id)
299
+
300
+ manager._start_cpu_sandbox_preload = fake_start_cpu_sandbox_preload # type: ignore[method-assign]
301
+
302
+ result = await manager.ensure_session_loaded(
303
+ "s1",
304
+ user_id="owner",
305
+ hf_token="new-token",
306
+ )
307
+
308
+ assert result is existing
309
+ assert existing.hf_token == "new-token"
310
+ assert existing.session.hf_token == "new-token"
311
+ assert existing.session.sandbox_preload_error is None
312
+ assert existing.session.sandbox_preload_task is None
313
+ assert started == ["s1"]
314
+
315
+
316
+ @pytest.mark.asyncio
317
+ async def test_existing_session_does_not_retry_preload_when_disabled():
318
+ manager = _manager_with_store(NoopSessionStore())
319
+ existing = _runtime_agent_session("s1", user_id="owner", hf_token=None)
320
+ done_task = asyncio.get_running_loop().create_future()
321
+ done_task.set_result(None)
322
+ existing.session.sandbox_preload_task = done_task
323
+ existing.session.sandbox_preload_error = (
324
+ "No HF token available. Cannot create sandbox."
325
+ )
326
+ manager.sessions["s1"] = existing
327
+ started: list[str] = []
328
+
329
+ def fake_start_cpu_sandbox_preload(agent_session):
330
+ started.append(agent_session.session_id)
331
+
332
+ manager._start_cpu_sandbox_preload = fake_start_cpu_sandbox_preload # type: ignore[method-assign]
333
+
334
+ result = await manager.ensure_session_loaded(
335
+ "s1",
336
+ user_id="owner",
337
+ hf_token="new-token",
338
+ preload_sandbox=False,
339
+ )
340
+
341
+ assert result is existing
342
+ assert existing.hf_token == "new-token"
343
+ assert existing.session.hf_token == "new-token"
344
+ assert existing.session.sandbox_preload_error == (
345
+ "No HF token available. Cannot create sandbox."
346
+ )
347
+ assert started == []
348
+
349
+
350
+ @pytest.mark.asyncio
351
+ async def test_existing_session_does_not_restart_preload_after_teardown():
352
+ manager = _manager_with_store(NoopSessionStore())
353
+ existing = _runtime_agent_session("s1", user_id="owner", hf_token="token")
354
+ done_task = asyncio.get_running_loop().create_future()
355
+ done_task.set_result(None)
356
+ existing.session.sandbox = None
357
+ existing.session.sandbox_preload_task = done_task
358
+ existing.session.sandbox_preload_error = None
359
+ manager.sessions["s1"] = existing
360
+ started: list[str] = []
361
+
362
+ def fake_start_cpu_sandbox_preload(agent_session):
363
+ started.append(agent_session.session_id)
364
+
365
+ manager._start_cpu_sandbox_preload = fake_start_cpu_sandbox_preload # type: ignore[method-assign]
366
+
367
+ result = await manager.ensure_session_loaded(
368
+ "s1",
369
+ user_id="owner",
370
+ hf_token="token",
371
+ )
372
+
373
+ assert result is existing
374
+ assert existing.session.sandbox_preload_task is done_task
375
+ assert existing.session.sandbox_preload_error is None
376
+ assert started == []
377
+
378
+
379
  @pytest.mark.asyncio
380
  async def test_concurrent_lazy_restore_starts_only_one_agent_task():
381
  store = RestoreStore(delay=0.01)