Synced from monorepo
Synced from monorepo Changes: - Release a shell session's resources in one drop - Make the tools blocking-wait cap client-configurable and self-describing - Recognize API "exceeds budget" errors as context overflow - Retry /btw on model overload - Carry running background tasks and subagents across compaction - Require round-trip time for SDK liveness checks - Background-subagent completion reminders with a selectable delivery surface - Make a PTY shell reap itself until it reaches the registry - Recover the OS error code from a TLS-phase connection reset - Consume the attached-client signal and report why idle is withheld - Treat `.grok/sandbox.toml` edits as protected so auto mode prompts before writing - Surface history/search in the Ctrl+. cheatsheet and keep it working in history view - Delete sessions from the dashboard and welcome list - Release a session's activity record when the session ends - Stop charging auth-retry budget for fail-closed 401s; reset it across suspends - Scope skills watches on project vendor roots - Make [stop] cancel in-flight compaction - Make the leader soak measure the leader, not its harness Source-Revision: 8d69c91f02bcacf01e98d5aebbf2f92547c45738
This commit is contained in:
parent
dd04f397b1
commit
a422116582
165 changed files with 15161 additions and 1969 deletions
|
|
@ -7,7 +7,9 @@ use tokio::sync::{mpsc, oneshot};
|
|||
|
||||
use crate::auth::config::LEGACY_AUTH_SCOPE;
|
||||
use crate::auth::{AuthManager, GrokAuth, GrokComConfig, parse_output};
|
||||
use crate::http::TransportFailureKind;
|
||||
use crate::util::grok_home;
|
||||
use xai_grok_telemetry::events::{LoginFailed, LoginFailureKind};
|
||||
|
||||
pub type StderrCallback = Box<dyn Fn(&str)>;
|
||||
|
||||
|
|
@ -451,6 +453,9 @@ pub async fn run_auth_flow_interactive(
|
|||
.await
|
||||
}
|
||||
|
||||
/// Every interactive login returns through here, so reporting the failure here
|
||||
/// costs one event per attempt — a retried request, or the discovery cache
|
||||
/// background token refresh shares, can't inflate it. Never changes the result.
|
||||
async fn run_auth_flow_inner(
|
||||
auth_manager: &Arc<AuthManager>,
|
||||
grok_com_config: &GrokComConfig,
|
||||
|
|
@ -460,6 +465,64 @@ async fn run_auth_flow_inner(
|
|||
url_tx: Option<Rc<RefCell<Option<oneshot::Sender<AuthUrlInfo>>>>>,
|
||||
code_rx: Option<mpsc::Receiver<String>>,
|
||||
login_override: LoginTransportOverride,
|
||||
) -> anyhow::Result<(GrokAuth, bool)> {
|
||||
let result = run_auth_flow_steps(
|
||||
auth_manager,
|
||||
grok_com_config,
|
||||
reauth,
|
||||
force_interactive,
|
||||
on_stderr,
|
||||
url_tx,
|
||||
code_rx,
|
||||
login_override,
|
||||
)
|
||||
.await;
|
||||
if let Err(err) = &result
|
||||
&& let Some(event) = login_failure_event(err)
|
||||
{
|
||||
xai_grok_telemetry::session_ctx::log_event(event);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// `None` when nothing in the chain failed over HTTP (the user backed out, the
|
||||
/// loopback listener couldn't bind, the id_token didn't validate) rather than
|
||||
/// inventing a transport verdict for it.
|
||||
fn login_failure_event(err: &anyhow::Error) -> Option<LoginFailed> {
|
||||
let source = err
|
||||
.chain()
|
||||
.find_map(|cause| cause.downcast_ref::<reqwest::Error>())?;
|
||||
Some(LoginFailed {
|
||||
error_kind: failure_kind(
|
||||
crate::http::TransportFailure::classify(source).kind,
|
||||
source.is_decode(),
|
||||
),
|
||||
os_error: crate::http::find_os_error_code(source),
|
||||
})
|
||||
}
|
||||
|
||||
/// A body that won't parse is a decode failure, not a transport one — even
|
||||
/// though `reqwest` also reports it as a body-phase error.
|
||||
fn failure_kind(transport: TransportFailureKind, is_decode: bool) -> LoginFailureKind {
|
||||
if is_decode {
|
||||
return LoginFailureKind::Decode;
|
||||
}
|
||||
match transport {
|
||||
TransportFailureKind::Unreachable => LoginFailureKind::TransportConnect,
|
||||
TransportFailureKind::Interrupted => LoginFailureKind::TransportInterrupted,
|
||||
TransportFailureKind::Permanent => LoginFailureKind::TransportPermanent,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_auth_flow_steps(
|
||||
auth_manager: &Arc<AuthManager>,
|
||||
grok_com_config: &GrokComConfig,
|
||||
reauth: bool,
|
||||
force_interactive: bool,
|
||||
on_stderr: Option<StderrCallback>,
|
||||
url_tx: Option<Rc<RefCell<Option<oneshot::Sender<AuthUrlInfo>>>>>,
|
||||
code_rx: Option<mpsc::Receiver<String>>,
|
||||
login_override: LoginTransportOverride,
|
||||
) -> anyhow::Result<(GrokAuth, bool)> {
|
||||
tracing::info!(
|
||||
has_oidc = grok_com_config.oidc.is_some(),
|
||||
|
|
@ -900,6 +963,41 @@ pub async fn run_cli_login(
|
|||
oauth: bool,
|
||||
device_auth: bool,
|
||||
devbox: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
// Devbox never reaches the login funnel, so it reports nothing and needs
|
||||
// no telemetry client — and `AuthManager::new` is not free (it logs, and
|
||||
// may rewrite auth.json to drop a stale scope).
|
||||
if devbox {
|
||||
let auth = super::devbox_login::run_devbox_login(config).await?;
|
||||
return apply_post_login_config(auth).await;
|
||||
}
|
||||
|
||||
// Agent bootstrap is what normally initializes the product telemetry
|
||||
// client, and `grok login` never boots an agent, so without this every
|
||||
// event this process emits is dropped before reaching a sink. One manager
|
||||
// serves both the identity it reads and the login flow below.
|
||||
let auth_manager = Arc::new(AuthManager::new(
|
||||
&grok_home::grok_home(),
|
||||
config.grok_com_config.clone(),
|
||||
));
|
||||
crate::agent::init::update_telemetry_config(config, &auth_manager);
|
||||
|
||||
let result = run_cli_login_steps(config, &auth_manager, oauth, device_auth).await;
|
||||
|
||||
// Posts run on a spawned task and this process exits as soon as we return.
|
||||
xai_grok_telemetry::session_ctx::drain_pending(CLI_TELEMETRY_DRAIN).await;
|
||||
result
|
||||
}
|
||||
|
||||
/// Returns as soon as the post lands (~1.7s cold), so the bound only bites on a
|
||||
/// black-holed network — where waiting out the HTTP client timeout would be worse.
|
||||
const CLI_TELEMETRY_DRAIN: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
|
||||
async fn run_cli_login_steps(
|
||||
config: &crate::agent::config::Config,
|
||||
auth_manager: &Arc<AuthManager>,
|
||||
oauth: bool,
|
||||
device_auth: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let login_override = LoginTransportOverride::from_flags(oauth, device_auth);
|
||||
|
||||
|
|
@ -908,15 +1006,11 @@ pub async fn run_cli_login(
|
|||
// supports the device flow. Without this guard, `grok login` on an
|
||||
// enterprise-OIDC deployment would wrongly enter the device branch (which
|
||||
// requires `oauth2`) and error.
|
||||
let authenticated = if devbox {
|
||||
super::devbox_login::run_devbox_login(config).await?
|
||||
} else if cli_should_use_device(&config.grok_com_config, login_override).await {
|
||||
let authenticated = if cli_should_use_device(&config.grok_com_config, login_override).await {
|
||||
if config.grok_com_config.oauth2.is_none() {
|
||||
// No OIDC and no oauth2 here, so `--oauth` can't help.
|
||||
anyhow::bail!("Sign-in is not available for this deployment. Set XAI_API_KEY instead.");
|
||||
}
|
||||
let grok_home = grok_home::grok_home();
|
||||
let auth_manager = Arc::new(AuthManager::new(&grok_home, config.grok_com_config.clone()));
|
||||
// Route through the shared inner flow (not `run_device_code_login`
|
||||
// directly) so the external auth provider and devbox auto-migration run
|
||||
// before the interactive device login. `force_interactive` skips the
|
||||
|
|
@ -925,7 +1019,7 @@ pub async fn run_cli_login(
|
|||
// Already resolved/logged above; pass `Preresolved(true)` so the inner flow
|
||||
// honors device without a second fetch or a duplicate `cli`-attributed log.
|
||||
let (auth, did_auth) = run_auth_flow_interactive(
|
||||
&auth_manager,
|
||||
auth_manager,
|
||||
&config.grok_com_config,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -945,21 +1039,35 @@ pub async fn run_cli_login(
|
|||
);
|
||||
}
|
||||
// Loopback. `reauth=true` clears creds up front (legacy-scope hygiene),
|
||||
// so abandoning logs you out — unlike the device branch above.
|
||||
// so abandoning logs you out — unlike the device branch above. Calls
|
||||
// `run_auth_flow` rather than `ensure_authenticated_with_override`,
|
||||
// which would build a second `AuthManager`; with `reauth` set and no
|
||||
// message prefix, the rest of that wrapper is a no-op.
|
||||
// Already resolved/logged above; pass `Preresolved(false)` so the inner
|
||||
// flow honors loopback without a duplicate `cli`-attributed log.
|
||||
ensure_authenticated_with_override(
|
||||
let (auth, did_auth) = run_auth_flow(
|
||||
auth_manager,
|
||||
&config.grok_com_config,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
LoginTransportOverride::Preresolved(false),
|
||||
)
|
||||
.await?
|
||||
.await?;
|
||||
if did_auth {
|
||||
report_signed_in(&auth);
|
||||
}
|
||||
auth
|
||||
};
|
||||
|
||||
// Sync this principal's config now rather than waiting for the background
|
||||
// tick. Stay quiet about absence/failure during login — confirm only when
|
||||
// config was actually applied; `grok setup` reports the no-config case.
|
||||
apply_post_login_config(authenticated).await
|
||||
}
|
||||
|
||||
/// Sync this principal's config now rather than waiting for the background
|
||||
/// tick. Stay quiet about absence/failure during login — confirm only when
|
||||
/// config was actually applied; `grok setup` reports the no-config case.
|
||||
async fn apply_post_login_config(authenticated: GrokAuth) -> anyhow::Result<()> {
|
||||
let outcome = crate::managed_config::post_login_sync(Some(authenticated)).await;
|
||||
match outcome {
|
||||
crate::managed_config::ManagedConfigSync::Updated { is_team: true } => {
|
||||
|
|
@ -1070,6 +1178,42 @@ mod tests {
|
|||
use crate::env::EnvVarGuard;
|
||||
use chrono::Utc;
|
||||
|
||||
/// `os_error` and the reqwest classification are covered in
|
||||
/// `xai-grok-http`; what's local is which `LoginFailureKind` each maps to,
|
||||
/// and that a decode failure never reads as a transport one.
|
||||
#[test]
|
||||
fn failure_kinds_map_one_to_one() {
|
||||
assert_eq!(
|
||||
failure_kind(TransportFailureKind::Unreachable, false),
|
||||
LoginFailureKind::TransportConnect
|
||||
);
|
||||
assert_eq!(
|
||||
failure_kind(TransportFailureKind::Interrupted, false),
|
||||
LoginFailureKind::TransportInterrupted
|
||||
);
|
||||
assert_eq!(
|
||||
failure_kind(TransportFailureKind::Permanent, false),
|
||||
LoginFailureKind::TransportPermanent
|
||||
);
|
||||
assert_eq!(
|
||||
failure_kind(TransportFailureKind::Interrupted, true),
|
||||
LoginFailureKind::Decode
|
||||
);
|
||||
}
|
||||
|
||||
/// A login that never reached the network is not a transport failure. The
|
||||
/// positive path needs a real `reqwest::Error`, and building a client here
|
||||
/// flips `jsonwebtoken` into its "no CryptoProvider" panic and breaks
|
||||
/// unrelated auth tests, so classification is covered in `xai-grok-http`.
|
||||
#[test]
|
||||
fn non_http_login_failures_are_not_reported() {
|
||||
let abandoned = anyhow::anyhow!("Login timed out after 10 minutes. Please try again.");
|
||||
assert!(login_failure_event(&abandoned).is_none());
|
||||
|
||||
let nested = abandoned.context("Login failed. Please try again.");
|
||||
assert!(login_failure_event(&nested).is_none());
|
||||
}
|
||||
|
||||
/// Run `f` with `GROK_LOGIN_DEVICE_FLOW` set to `value` (unset for `None`).
|
||||
/// `EnvVarGuard` serializes the process env and restores it on drop, so
|
||||
/// `resolve_device_flow` reads the env tier from a known state.
|
||||
|
|
|
|||
Loading…
Reference in a new issue