Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Upload 4 files
Browse files
README.md
CHANGED
|
@@ -133,9 +133,9 @@ See `CHANGELOG_V194.md` for the Model Pre-scan Decision Card UI pass.
|
|
| 133 |
See `CHANGELOG_V195.md` for the Runtime CSS Cleanup with Legacy Safety Net pass.
|
| 134 |
|
| 135 |
|
| 136 |
-
Current release: Agentic Space Factory v198.26.
|
| 137 |
|
| 138 |
|
| 139 |
-
## v198.26.
|
| 140 |
|
| 141 |
-
See `
|
|
|
|
| 133 |
See `CHANGELOG_V195.md` for the Runtime CSS Cleanup with Legacy Safety Net pass.
|
| 134 |
|
| 135 |
|
| 136 |
+
Current release: Agentic Space Factory v198.26.12.
|
| 137 |
|
| 138 |
|
| 139 |
+
## v198.26.12 — OAuth Recovery
|
| 140 |
|
| 141 |
+
See `CHANGELOG_V198_26_12.md`.
|
app.py
CHANGED
|
@@ -97,6 +97,53 @@ def _oauth_context_from_request(request: Request) -> dict[str, Any]:
|
|
| 97 |
"warnings": oauth_warning_messages(ctx),
|
| 98 |
}
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
def _json_error(exc: Exception) -> HTTPException:
|
| 101 |
if isinstance(exc, HTTPException):
|
| 102 |
return exc
|
|
@@ -548,6 +595,37 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
|
|
| 548 |
# redirect without custom target parameters to avoid OAuth loops.
|
| 549 |
return RedirectResponse("/oauth/huggingface/logout", status_code=307)
|
| 550 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
@fastapi_app.get("/", response_class=HTMLResponse)
|
| 552 |
@fastapi_app.get("/custom", response_class=HTMLResponse)
|
| 553 |
async def custom_index(): # type: ignore[no-untyped-def]
|
|
@@ -589,16 +667,24 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
|
|
| 589 |
"custom_ui_status": "root_custom_ui",
|
| 590 |
"user": {"username": ctx["username"], "missing_scopes": ctx.get("missing_scopes", []), "warnings": ctx.get("warnings", []), "auth_lifetime": ctx.get("auth_lifetime")} if ctx else None,
|
| 591 |
"login_url": "/oauth/huggingface/login",
|
| 592 |
-
"
|
|
|
|
|
|
|
| 593 |
"anonymous_eval": public_eval_config(ctx["username"] if ctx else None),
|
| 594 |
}
|
| 595 |
)
|
| 596 |
|
| 597 |
@fastapi_app.get("/api/me")
|
| 598 |
async def api_me(request: Request): # type: ignore[no-untyped-def]
|
| 599 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 600 |
return JSONResponse(
|
| 601 |
{
|
|
|
|
| 602 |
"username": ctx["username"],
|
| 603 |
"profile": {
|
| 604 |
"name": ctx["profile"].get("name"),
|
|
@@ -614,7 +700,9 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
|
|
| 614 |
"anonymous_eval": public_eval_config(ctx["username"] if ctx else None),
|
| 615 |
"expires_at": ctx.get("expires_at"),
|
| 616 |
"login_url": "/oauth/huggingface/login",
|
| 617 |
-
"
|
|
|
|
|
|
|
| 618 |
}
|
| 619 |
)
|
| 620 |
|
|
@@ -723,7 +811,10 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
|
|
| 723 |
"reason": exc.detail,
|
| 724 |
"oauth_env": env_status,
|
| 725 |
"login_url": "/oauth/huggingface/login",
|
| 726 |
-
"
|
|
|
|
|
|
|
|
|
|
| 727 |
}
|
| 728 |
)
|
| 729 |
return JSONResponse(
|
|
@@ -733,7 +824,9 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
|
|
| 733 |
"token_identity": verify_token_identity(ctx),
|
| 734 |
"oauth_env": env_status,
|
| 735 |
"login_url": "/oauth/huggingface/login",
|
| 736 |
-
"
|
|
|
|
|
|
|
| 737 |
}
|
| 738 |
)
|
| 739 |
|
|
|
|
| 97 |
"warnings": oauth_warning_messages(ctx),
|
| 98 |
}
|
| 99 |
|
| 100 |
+
|
| 101 |
+
def _clear_local_oauth_session(request: Request) -> dict[str, Any]:
|
| 102 |
+
"""Clear ASF/HF OAuth session state without requiring a valid token.
|
| 103 |
+
|
| 104 |
+
This is intentionally tolerant: expired OAuth sessions must be recoverable
|
| 105 |
+
even when parse_huggingface_oauth/extract_oauth_context can no longer
|
| 106 |
+
deserialize or validate the token.
|
| 107 |
+
"""
|
| 108 |
+
cleared_keys: list[str] = []
|
| 109 |
+
had_session = False
|
| 110 |
+
try:
|
| 111 |
+
session = request.session # type: ignore[attr-defined]
|
| 112 |
+
had_session = bool(session)
|
| 113 |
+
for key in list(session.keys()):
|
| 114 |
+
key_l = str(key).lower()
|
| 115 |
+
if "oauth" in key_l or "huggingface" in key_l or key_l in {"state", "nonce", "next", "redirect_uri"}:
|
| 116 |
+
cleared_keys.append(str(key))
|
| 117 |
+
session.clear()
|
| 118 |
+
except Exception:
|
| 119 |
+
had_session = False
|
| 120 |
+
return {"cleared": True, "had_session": had_session, "cleared_keys": sorted(set(cleared_keys))}
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def _auth_recovery_payload(reason: str = "not_authenticated", detail: str | None = None) -> dict[str, Any]:
|
| 124 |
+
reason_l = (reason or "not_authenticated").lower()
|
| 125 |
+
if "expired" in reason_l or (detail and "expired" in detail.lower()):
|
| 126 |
+
reason_l = "oauth_expired"
|
| 127 |
+
message = "Your Hugging Face OAuth session expired. Refresh sign-in to continue."
|
| 128 |
+
else:
|
| 129 |
+
reason_l = "not_signed_in" if reason_l in {"not_authenticated", "please sign in with hugging face first."} else reason_l
|
| 130 |
+
message = "Sign in with Hugging Face to continue."
|
| 131 |
+
return {
|
| 132 |
+
"authenticated": False,
|
| 133 |
+
"reason": reason_l,
|
| 134 |
+
"message": message,
|
| 135 |
+
"login_url": "/oauth/huggingface/login",
|
| 136 |
+
"refresh_login_url": "/auth/refresh-login",
|
| 137 |
+
"reset_url": "/auth/logout-local",
|
| 138 |
+
"logout_url": "/auth/logout-local",
|
| 139 |
+
"recovery": {
|
| 140 |
+
"primary_action": "refresh_sign_in" if reason_l == "oauth_expired" else "sign_in",
|
| 141 |
+
"refresh_login_url": "/auth/refresh-login",
|
| 142 |
+
"reset_local_auth_url": "/auth/logout-local",
|
| 143 |
+
"open_in_new_tab_recommended": True,
|
| 144 |
+
},
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
def _json_error(exc: Exception) -> HTTPException:
|
| 148 |
if isinstance(exc, HTTPException):
|
| 149 |
return exc
|
|
|
|
| 595 |
# redirect without custom target parameters to avoid OAuth loops.
|
| 596 |
return RedirectResponse("/oauth/huggingface/logout", status_code=307)
|
| 597 |
|
| 598 |
+
@fastapi_app.get("/auth/logout-local")
|
| 599 |
+
async def auth_logout_local(request: Request): # type: ignore[no-untyped-def]
|
| 600 |
+
# Local reset must work even when the HF OAuth token is expired and the
|
| 601 |
+
# official helper can no longer parse the session.
|
| 602 |
+
_clear_local_oauth_session(request)
|
| 603 |
+
response = RedirectResponse("/", status_code=303)
|
| 604 |
+
response.delete_cookie("session")
|
| 605 |
+
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
|
| 606 |
+
response.headers["Pragma"] = "no-cache"
|
| 607 |
+
return response
|
| 608 |
+
|
| 609 |
+
@fastapi_app.get("/auth/refresh-login")
|
| 610 |
+
async def auth_refresh_login(request: Request): # type: ignore[no-untyped-def]
|
| 611 |
+
# Force a clean re-authorization path. This avoids stale oauth_info
|
| 612 |
+
# state after token expiry and should make Space duplication unnecessary.
|
| 613 |
+
_clear_local_oauth_session(request)
|
| 614 |
+
response = RedirectResponse("/oauth/huggingface/login", status_code=303)
|
| 615 |
+
response.delete_cookie("session")
|
| 616 |
+
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
|
| 617 |
+
response.headers["Pragma"] = "no-cache"
|
| 618 |
+
return response
|
| 619 |
+
|
| 620 |
+
@fastapi_app.get("/auth/logout-and-refresh")
|
| 621 |
+
async def auth_logout_and_refresh(request: Request): # type: ignore[no-untyped-def]
|
| 622 |
+
_clear_local_oauth_session(request)
|
| 623 |
+
response = RedirectResponse("/auth/refresh-login", status_code=303)
|
| 624 |
+
response.delete_cookie("session")
|
| 625 |
+
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
|
| 626 |
+
response.headers["Pragma"] = "no-cache"
|
| 627 |
+
return response
|
| 628 |
+
|
| 629 |
@fastapi_app.get("/", response_class=HTMLResponse)
|
| 630 |
@fastapi_app.get("/custom", response_class=HTMLResponse)
|
| 631 |
async def custom_index(): # type: ignore[no-untyped-def]
|
|
|
|
| 667 |
"custom_ui_status": "root_custom_ui",
|
| 668 |
"user": {"username": ctx["username"], "missing_scopes": ctx.get("missing_scopes", []), "warnings": ctx.get("warnings", []), "auth_lifetime": ctx.get("auth_lifetime")} if ctx else None,
|
| 669 |
"login_url": "/oauth/huggingface/login",
|
| 670 |
+
"refresh_login_url": "/auth/refresh-login",
|
| 671 |
+
"reset_auth_url": "/auth/logout-local",
|
| 672 |
+
"logout_url": "/auth/logout-local",
|
| 673 |
"anonymous_eval": public_eval_config(ctx["username"] if ctx else None),
|
| 674 |
}
|
| 675 |
)
|
| 676 |
|
| 677 |
@fastapi_app.get("/api/me")
|
| 678 |
async def api_me(request: Request): # type: ignore[no-untyped-def]
|
| 679 |
+
try:
|
| 680 |
+
ctx = _oauth_context_from_request(request)
|
| 681 |
+
except HTTPException as exc:
|
| 682 |
+
payload = _auth_recovery_payload(str(exc.detail), str(exc.detail))
|
| 683 |
+
payload["anonymous_eval"] = public_eval_config(None)
|
| 684 |
+
return JSONResponse(payload, status_code=401)
|
| 685 |
return JSONResponse(
|
| 686 |
{
|
| 687 |
+
"authenticated": True,
|
| 688 |
"username": ctx["username"],
|
| 689 |
"profile": {
|
| 690 |
"name": ctx["profile"].get("name"),
|
|
|
|
| 700 |
"anonymous_eval": public_eval_config(ctx["username"] if ctx else None),
|
| 701 |
"expires_at": ctx.get("expires_at"),
|
| 702 |
"login_url": "/oauth/huggingface/login",
|
| 703 |
+
"refresh_login_url": "/auth/refresh-login",
|
| 704 |
+
"reset_url": "/auth/logout-local",
|
| 705 |
+
"logout_url": "/auth/logout-local",
|
| 706 |
}
|
| 707 |
)
|
| 708 |
|
|
|
|
| 811 |
"reason": exc.detail,
|
| 812 |
"oauth_env": env_status,
|
| 813 |
"login_url": "/oauth/huggingface/login",
|
| 814 |
+
"refresh_login_url": "/auth/refresh-login",
|
| 815 |
+
"reset_url": "/auth/logout-local",
|
| 816 |
+
"logout_url": "/auth/logout-local",
|
| 817 |
+
"recommended_action": "refresh_sign_in" if "expired" in str(exc.detail).lower() else "sign_in",
|
| 818 |
}
|
| 819 |
)
|
| 820 |
return JSONResponse(
|
|
|
|
| 824 |
"token_identity": verify_token_identity(ctx),
|
| 825 |
"oauth_env": env_status,
|
| 826 |
"login_url": "/oauth/huggingface/login",
|
| 827 |
+
"refresh_login_url": "/auth/refresh-login",
|
| 828 |
+
"reset_url": "/auth/logout-local",
|
| 829 |
+
"logout_url": "/auth/logout-local",
|
| 830 |
}
|
| 831 |
)
|
| 832 |
|