@@ -297,11 +297,23 @@ def _grok_oauth_display_status(value: object) -> str:
297297 return "normal"
298298 if probe_status == "limited" :
299299 return "limited"
300- if probe_status in { "invalid" , "unknown" } :
300+ if probe_status == "invalid" :
301301 return "invalid"
302+ if probe_status == "unknown" :
303+ return "unknown"
302304 return "normal" if status == "active" else "invalid"
303305
304306
307+ def _grok_oauth_probe_recoverable (value : object ) -> bool :
308+ probe = value if isinstance (value , dict ) else {}
309+ if _clean_text (probe .get ("status" )).lower () != "invalid" :
310+ return False
311+ code = _clean_text (probe .get ("code" )).lower ()
312+ return code != "permission-denied" and not (
313+ code == "personal-team-blocked" or code .startswith ("personal-team-blocked:" )
314+ )
315+
316+
305317def _grok_oauth_authorizable (item : object ) -> bool :
306318 """Return whether an SSO registration record is ready for OAuth."""
307319 if not isinstance (item , dict ):
@@ -1548,7 +1560,7 @@ def _grok_status_matches(item: dict[str, Any], status: str) -> bool:
15481560 if oauth_status == "denied" :
15491561 authorization = item .get ("oauth_authorization" ) if isinstance (item .get ("oauth_authorization" ), dict ) else {}
15501562 return not isinstance (item .get ("oauth" ), dict ) and _clean_text (authorization .get ("status" )).lower () == "denied"
1551- if oauth_status in {"unauthorized" , "normal" , "limited" , "no_quota" , "expired" , "invalid" }:
1563+ if oauth_status in {"unauthorized" , "normal" , "limited" , "no_quota" , "expired" , "invalid" , "unknown" }:
15521564 if oauth_status == "unauthorized" :
15531565 return _grok_oauth_authorizable (item ) and not isinstance (item .get ("oauth" ), dict )
15541566 return _grok_oauth_display_status (item .get ("oauth" )) == oauth_status
@@ -1689,7 +1701,7 @@ def grok_accounts_view(self, *, keyword: str = "", status: str = "all") -> dict[
16891701 "oauth_linked" : sum (1 for item in merged_items if isinstance (item .get ("oauth" ), dict )),
16901702 "oauth_status" : {
16911703 status : sum (1 for value in oauth_display_statuses if value == status )
1692- for status in ("unauthorized" , "denied" , "normal" , "limited" , "no_quota" , "expired" , "invalid" )
1704+ for status in ("unauthorized" , "denied" , "normal" , "limited" , "no_quota" , "expired" , "invalid" , "unknown" )
16931705 },
16941706 "runtime_status" : runtime_status ,
16951707 "calls_total" : calls_total ,
@@ -2272,7 +2284,7 @@ def _recover_grok_oauth_account(
22722284 if not oauth_account_id or _clean_text (oauth_item .get ("status" )).lower () == "disabled" :
22732285 return {"status" : "deferred" }
22742286 probe = oauth_item .get ("probe" ) if isinstance (oauth_item .get ("probe" ), dict ) else {}
2275- if _clean_text (probe . get ( "status" )). lower () != "invalid" :
2287+ if not _grok_oauth_probe_recoverable (probe ) :
22762288 return {"status" : "deferred" }
22772289
22782290 now = datetime .now (timezone .utc )
@@ -2375,10 +2387,7 @@ def _grok_oauth_has_inflight_recovery() -> bool:
23752387 return False
23762388 return any (
23772389 _clean_text (item .get ("status" )).lower () != "disabled"
2378- and _clean_text (
2379- item .get ("probe" , {}).get ("status" ) if isinstance (item .get ("probe" ), dict ) else ""
2380- ).lower ()
2381- == "invalid"
2390+ and _grok_oauth_probe_recoverable (item .get ("probe" ))
23822391 and _clean_text (
23832392 item .get ("recovery" , {}).get ("status" ) if isinstance (item .get ("recovery" ), dict ) else ""
23842393 ).lower ()
@@ -2387,6 +2396,21 @@ def _grok_oauth_has_inflight_recovery() -> bool:
23872396 if isinstance (item , dict )
23882397 )
23892398
2399+ @staticmethod
2400+ def _grok_oauth_has_due_invalid_recovery () -> bool :
2401+ try :
2402+ items = xai_cli_oauth_store .list_accounts (redacted = True )
2403+ except Exception :
2404+ return False
2405+ now = datetime .now (timezone .utc )
2406+ return any (
2407+ _clean_text (item .get ("status" )).lower () != "disabled"
2408+ and _grok_oauth_probe_recoverable (item .get ("probe" ))
2409+ and RegisterService ._grok_oauth_recovery_due (item , now = now )
2410+ for item in items
2411+ if isinstance (item , dict )
2412+ )
2413+
23902414 @staticmethod
23912415 def _grok_oauth_unlinked_sources () -> tuple [list [dict [str , Any ]], int ]:
23922416 oauth_emails = {
@@ -2422,6 +2446,19 @@ def _grok_oauth_has_unlinked_accounts(self) -> bool:
24222446 return False
24232447 return bool (sources )
24242448
2449+ def handle_grok_oauth_probe_results (self , results : list [dict [str , Any ]]) -> None :
2450+ has_invalid = any (
2451+ isinstance (item , dict )
2452+ and _grok_oauth_probe_recoverable (item )
2453+ for item in results
2454+ )
2455+ if not has_invalid :
2456+ return
2457+ with self ._lock :
2458+ enabled = bool (self ._grok_probe_config_locked ().get ("enabled" ))
2459+ if enabled :
2460+ self ._grok_probe_wake_event .set ()
2461+
24252462 def _queue_unlinked_grok_oauth_accounts (
24262463 self ,
24272464 * ,
@@ -2571,6 +2608,7 @@ def _run_grok_probe_scheduler(self, stop_event: threading.Event) -> None:
25712608 recovery_sweep_due = (
25722609 recovery_sweep_due
25732610 or self ._grok_oauth_has_inflight_recovery ()
2611+ or self ._grok_oauth_has_due_invalid_recovery ()
25742612 or self ._grok_oauth_has_unlinked_accounts ()
25752613 )
25762614 if recovery_sweep_due and not cycle_stop .is_set ():
@@ -2583,6 +2621,12 @@ def _run_grok_probe_scheduler(self, stop_event: threading.Event) -> None:
25832621 permission_retry = self ._run_grok_permission_retry_once (cycle_stop )
25842622 if cycle_stop .is_set ():
25852623 continue
2624+ if self ._grok_oauth_has_due_invalid_recovery ():
2625+ self ._run_grok_oauth_recovery_sweep (
2626+ include_backfill = False ,
2627+ stop_event = cycle_stop ,
2628+ )
2629+ continue
25862630 with self ._lock :
25872631 probe = self ._grok_probe_config_locked ()
25882632 should_run = bool (probe .get ("enabled" )) and self ._grok_probe_due_locked (probe )
@@ -2709,6 +2753,7 @@ def _run_grok_oauth_recovery_sweep(
27092753 * ,
27102754 limit : int | None = None ,
27112755 reclaim_inflight : bool = False ,
2756+ include_backfill : bool = True ,
27122757 stop_event : Any | None = None ,
27132758 ) -> dict [str , int ]:
27142759 summary = {
@@ -2734,14 +2779,7 @@ def _run_grok_oauth_recovery_sweep(
27342779 item
27352780 for item in oauth_items
27362781 if _clean_text (item .get ("status" )).lower () != "disabled"
2737- and _clean_text (
2738- item .get ("probe" , {}).get ("status" ) if isinstance (item .get ("probe" ), dict ) else ""
2739- ).lower ()
2740- == "invalid"
2741- and _clean_text (
2742- item .get ("probe" , {}).get ("code" ) if isinstance (item .get ("probe" ), dict ) else ""
2743- ).lower ()
2744- != "permission-denied"
2782+ and _grok_oauth_probe_recoverable (item .get ("probe" ))
27452783 ]
27462784 candidates .sort (
27472785 key = lambda item : 0
@@ -2781,9 +2819,10 @@ def _run_grok_oauth_recovery_sweep(
27812819 summary ["failed" ] += 1
27822820 else :
27832821 summary ["deferred" ] += 1
2784- backfill = self ._queue_unlinked_grok_oauth_accounts (stop_event = stop_event )
2785- for key , value in backfill .items ():
2786- summary [f"backfill_{ key } " ] = value
2822+ if include_backfill :
2823+ backfill = self ._queue_unlinked_grok_oauth_accounts (stop_event = stop_event )
2824+ for key , value in backfill .items ():
2825+ summary [f"backfill_{ key } " ] = value
27872826 except _GrokProbeCancelled :
27882827 cancelled = True
27892828 except Exception as error :
@@ -2996,7 +3035,7 @@ def _run_grok_probe_once(self, stop_event: Any | None = None) -> dict[str, int]:
29963035 status = _clean_text (result .get ("status" )).lower () if isinstance (result , dict ) else "unknown"
29973036 key = status if status in {"valid" , "limited" , "invalid" , "unknown" } else "unknown"
29983037 summary [f"oauth_{ key } " ] += 1
2999- if status != "invalid" or not isinstance (result , dict ):
3038+ if not isinstance ( result , dict ) or not _grok_oauth_probe_recoverable (result ):
30003039 continue
30013040 oauth_account_id = _clean_text (result .get ("account_id" ))
30023041 candidate = oauth_by_id .get (oauth_account_id )
@@ -3005,7 +3044,14 @@ def _run_grok_probe_once(self, stop_event: Any | None = None) -> dict[str, int]:
30053044 continue
30063045 _raise_if_grok_probe_cancelled (stop_event )
30073046 recovery = self ._recover_grok_oauth_account (
3008- {** candidate , "status" : "invalid" , "probe" : {"status" : "invalid" }},
3047+ {
3048+ ** candidate ,
3049+ "status" : "invalid" ,
3050+ "probe" : {
3051+ "status" : "invalid" ,
3052+ "code" : _clean_text (result .get ("code" )),
3053+ },
3054+ },
30093055 source_by_email ,
30103056 )
30113057 recovery_status = _clean_text (recovery .get ("status" )).lower ()
@@ -4203,3 +4249,4 @@ def _start_xai_cli_oauth_protocol(
42034249from services .xai_cli_oauth_service import xai_cli_oauth_service
42044250
42054251xai_cli_oauth_service .protocol_event_sink = register_service .handle_grok_oauth_protocol_event
4252+ xai_cli_oauth_service .probe_event_sink = register_service .handle_grok_oauth_probe_results
0 commit comments