Synced from monorepo

Synced from monorepo

Changes:
- Refresh tool search when the managed MCP catalog is re-fetched
- Prevent duplicate leader process spawn and startup hang from stale leaders
- Document marketplaces, plugins, and organization controls
- Stamp session ID on image generation direct-to-API requests
- Fix auto mode blocked documentation
- Auto mode considers recent user intent
- Expose deploy archive, taken-down, limit, and in-progress reasons on the chat API
- Fail-closed auth refresh contract for shell clients
- Emit a chat-supplied per-session turn index in turn hooks
- Show bash mode chrome in minimal mode
- Add metrics for true-noop and stationarity stops
- Include voice interim text on prompt submit
- Silently end turn on true-noop thrash
- Quiet copy toast when clipboard delivery is confirmed
- Fix session fork truncating at the wrong prompt in rewound sessions
- Make the idle "still running" watcher cue clickable to open the tasks pane
- Default web search model to grok-4.5
- Let plugin subagents inherit parent MCP servers
- Gate no-op end-turn reminder on system reminders
- Add gateway bridge lifecycle telemetry
- Allow editing finalized text while voice is open
- Relocate token carrier to turn-commit events and plumb per-turn origin context
- Raise workflow scratch quotas and make failed runs resumable
- Workflows overlay: auto-progress phases, live agent status, and drop budget meter

Source-Revision: 9b8d35b46d959c042ea9aa31cbbebbd1f0c5c527
This commit is contained in:
grokkybara[bot] 2026-07-24 16:59:42 +00:00
commit 6e38642082
103 changed files with 4964 additions and 1261 deletions

View file

@ -159,6 +159,15 @@ pub struct PrincipalKey {
fingerprint: String,
}
impl PrincipalKey {
/// Stable non-secret fingerprint (e.g. OIDC issuer+client); never tokens.
pub fn opaque(fingerprint: impl Into<String>) -> Self {
Self {
fingerprint: fingerprint.into(),
}
}
}
impl fmt::Debug for PrincipalKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PrincipalKey").finish_non_exhaustive()

View file

@ -467,6 +467,79 @@ mod inner {
pub(crate) fn admission_wait_observe(secs: f64) {
ADMISSION_WAIT_SECONDS.observe(secs);
}
// ── OIDC refresh (auth.current path) ────────────────────────────
/// Closed-set outcomes for `AuthProvider::current` (metric labels).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OidcRefreshOutcome {
SkippedNotExpired,
Ok,
FailedUsedStale,
}
impl OidcRefreshOutcome {
pub const fn as_str(self) -> &'static str {
match self {
Self::SkippedNotExpired => "skipped_not_expired",
Self::Ok => "ok",
Self::FailedUsedStale => "failed_used_stale",
}
}
}
static OIDC_REFRESH_TOTAL: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
"computer_hub_oidc_refresh_total",
"OIDC AuthProvider::current outcomes: skipped_not_expired (no network), \
ok (refresh succeeded), failed_used_stale (refresh failed, stale token returned).",
&["outcome"]
)
.expect("computer_hub_oidc_refresh_total must register once")
});
static OIDC_REFRESH_DURATION_SECONDS: LazyLock<Histogram> = LazyLock::new(|| {
register_histogram!(
"computer_hub_oidc_refresh_duration_seconds",
"Wall-clock time of an attempted OIDC refresh (discovery + token exchange). \
Not sampled for skipped_not_expired.",
exponential_buckets(0.01, 2.0, 14).expect("valid bucket params")
)
.expect("computer_hub_oidc_refresh_duration_seconds must register once")
});
/// Duration observed only for attempted refreshes (`Ok` / `FailedUsedStale`).
pub(crate) fn oidc_refresh_observe(outcome: OidcRefreshOutcome, secs: Option<f64>) {
OIDC_REFRESH_TOTAL
.with_label_values(&[outcome.as_str()])
.inc();
if let Some(secs) = secs {
OIDC_REFRESH_DURATION_SECONDS.observe(secs);
}
}
#[cfg(test)]
pub(crate) fn oidc_refresh_count(outcome: OidcRefreshOutcome) -> u64 {
OIDC_REFRESH_TOTAL
.with_label_values(&[outcome.as_str()])
.get()
}
#[cfg(test)]
pub(crate) fn oidc_refresh_duration_sample_count() -> u64 {
OIDC_REFRESH_DURATION_SECONDS.get_sample_count()
}
/// Serializes OIDC metric delta assertions under parallel `cargo test`.
#[cfg(test)]
static OIDC_METRICS_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
pub(crate) fn lock_oidc_metrics_test() -> std::sync::MutexGuard<'static, ()> {
OIDC_METRICS_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
}
#[cfg(not(feature = "metrics"))]
@ -507,8 +580,16 @@ mod inner {
pub(crate) fn tool_call_inflight_inc(_scope: &str) {}
pub(crate) fn tool_call_inflight_dec(_scope: &str) {}
pub(crate) fn admission_wait_observe(_secs: f64) {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OidcRefreshOutcome {
SkippedNotExpired,
Ok,
FailedUsedStale,
}
pub(crate) fn oidc_refresh_observe(_outcome: OidcRefreshOutcome, _secs: Option<f64>) {}
}
pub(crate) use inner::OidcRefreshOutcome;
pub(crate) use inner::admission_wait_observe;
pub(crate) use inner::call_dispatch_observe;
pub(crate) use inner::call_id_collision;
@ -524,8 +605,15 @@ pub(crate) use inner::inbox_full_notification_dropped;
pub(crate) use inner::inbox_full_reject_send_failed;
pub(crate) use inner::inbox_full_request_rejected;
pub(crate) use inner::liveness_deadline_expired;
#[cfg(all(test, feature = "metrics"))]
pub(crate) use inner::lock_oidc_metrics_test;
pub(crate) use inner::no_handler;
pub(crate) use inner::notif_lagged_recovered;
#[cfg(all(test, feature = "metrics"))]
pub(crate) use inner::oidc_refresh_count;
#[cfg(all(test, feature = "metrics"))]
pub(crate) use inner::oidc_refresh_duration_sample_count;
pub(crate) use inner::oidc_refresh_observe;
pub(crate) use inner::pool_connections_dec;
pub(crate) use inner::pool_connections_inc;
pub(crate) use inner::pool_evictions_inc;

View file

@ -131,13 +131,46 @@ impl AuthProvider for OidcAuthProvider {
Utc::now() + chrono::Duration::from_std(REFRESH_MARGIN).unwrap() >= exp
})
};
if expired && let Err(e) = self.try_refresh() {
tracing::warn!(error = %e, "OIDC refresh failed, using stale token");
if !expired {
crate::metrics::oidc_refresh_observe(
crate::metrics::OidcRefreshOutcome::SkippedNotExpired,
None,
);
} else {
use crate::metrics::{OidcRefreshOutcome, oidc_refresh_observe};
let started = std::time::Instant::now();
match self.try_refresh() {
Ok(()) => {
let secs = started.elapsed().as_secs_f64();
oidc_refresh_observe(OidcRefreshOutcome::Ok, Some(secs));
tracing::info!(duration_secs = secs, outcome = "ok", "OIDC token refreshed");
}
Err(e) => {
let secs = started.elapsed().as_secs_f64();
oidc_refresh_observe(OidcRefreshOutcome::FailedUsedStale, Some(secs));
tracing::warn!(
error = %e,
duration_secs = secs,
outcome = "failed_used_stale",
"OIDC refresh failed, using stale token"
);
}
}
}
let s = self.state.lock();
AuthCredential::bearer(&s.access_token)
}
/// Stable issuer/client/user pool key; does not call [`Self::current`].
fn principal_key(&self) -> crate::auth::PrincipalKey {
let mut fingerprint = format!("oidc:{}:{}", self.issuer, self.client_id);
if let Some(uid) = self.user_id.as_deref() {
fingerprint.push(':');
fingerprint.push_str(uid);
}
crate::auth::PrincipalKey::opaque(fingerprint)
}
/// Surface the principal fields parsed from the auth source. `None` only
/// when no `user_id` was supplied (nothing to attribute).
fn identity(&self) -> Option<AuthIdentity> {
@ -219,8 +252,6 @@ impl OidcAuthProvider {
.expires_in
.map(|s| Utc::now() + chrono::Duration::seconds(s as i64));
tracing::info!(expires_at = ?expires_at, "OIDC token refreshed");
if let Some(ref cb) = self.on_refresh {
cb(&RefreshEvent {
access_token: tokens.access_token.clone(),
@ -245,6 +276,8 @@ mod tests {
#[test]
fn current_returns_token_when_not_expired() {
#[cfg(feature = "metrics")]
let _guard = crate::metrics::lock_oidc_metrics_test();
let provider = OidcAuthProviderBuilder::new(
"access-tok",
"refresh-tok",
@ -265,6 +298,8 @@ mod tests {
#[test]
fn current_returns_token_when_no_expiry() {
#[cfg(feature = "metrics")]
let _guard = crate::metrics::lock_oidc_metrics_test();
let provider = OidcAuthProviderBuilder::new(
"no-expiry-tok",
"refresh-tok",
@ -282,6 +317,8 @@ mod tests {
#[test]
fn current_returns_stale_token_when_refresh_fails() {
#[cfg(feature = "metrics")]
let _guard = crate::metrics::lock_oidc_metrics_test();
// Expired token, but issuer is unreachable — should return stale
let provider = OidcAuthProviderBuilder::new(
"stale-tok",
@ -299,6 +336,162 @@ mod tests {
}
}
#[cfg(feature = "metrics")]
#[test]
fn current_records_skipped_and_failed_refresh_outcomes() {
let _guard = crate::metrics::lock_oidc_metrics_test();
use crate::metrics::OidcRefreshOutcome;
let skipped_before =
crate::metrics::oidc_refresh_count(OidcRefreshOutcome::SkippedNotExpired);
let failed_before = crate::metrics::oidc_refresh_count(OidcRefreshOutcome::FailedUsedStale);
let duration_before = crate::metrics::oidc_refresh_duration_sample_count();
let fresh = OidcAuthProviderBuilder::new(
"access-tok",
"refresh-tok",
"https://auth.example.com",
"client1",
)
.expires_at(Utc::now() + chrono::Duration::hours(1))
.build();
let _ = fresh.current();
assert_eq!(
crate::metrics::oidc_refresh_count(OidcRefreshOutcome::SkippedNotExpired),
skipped_before + 1
);
assert_eq!(
crate::metrics::oidc_refresh_duration_sample_count(),
duration_before,
"skipped path must not observe refresh duration"
);
let stale = OidcAuthProviderBuilder::new(
"stale-tok",
"refresh-tok",
"https://localhost:1",
"client1",
)
.expires_at(Utc::now() - chrono::Duration::hours(1))
.build();
let _ = stale.current();
assert_eq!(
crate::metrics::oidc_refresh_count(OidcRefreshOutcome::FailedUsedStale),
failed_before + 1
);
assert_eq!(
crate::metrics::oidc_refresh_duration_sample_count(),
duration_before + 1,
"failed refresh must observe exactly one duration sample"
);
assert_eq!(
crate::metrics::oidc_refresh_count(OidcRefreshOutcome::SkippedNotExpired),
skipped_before + 1,
"failed refresh must not also count as skipped"
);
}
#[test]
fn principal_key_is_stable_and_does_not_call_current() {
#[cfg(feature = "metrics")]
let _guard = crate::metrics::lock_oidc_metrics_test();
#[cfg(feature = "metrics")]
let skipped_before = crate::metrics::oidc_refresh_count(
crate::metrics::OidcRefreshOutcome::SkippedNotExpired,
);
let provider = OidcAuthProviderBuilder::new(
"access-tok",
"refresh-tok",
"https://auth.example.com",
"client1",
)
.user_id("user-9")
.expires_at(Utc::now() + chrono::Duration::hours(1))
.build();
let k1 = provider.principal_key();
let k2 = provider.principal_key();
assert_eq!(k1, k2);
#[cfg(feature = "metrics")]
assert_eq!(
crate::metrics::oidc_refresh_count(
crate::metrics::OidcRefreshOutcome::SkippedNotExpired
),
skipped_before,
"principal_key must not call current()"
);
let token_key = AuthCredential::bearer("access-tok").principal_key();
assert_ne!(k1, token_key);
}
#[cfg(feature = "metrics")]
#[allow(clippy::await_holding_lock)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn current_records_ok_refresh_outcome_against_mock_idp() {
use crate::metrics::OidcRefreshOutcome;
use axum::Router;
use axum::routing::{get, post};
let _guard = crate::metrics::lock_oidc_metrics_test();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let base = format!("http://{addr}");
let token_endpoint = format!("{base}/token");
let app = Router::new()
.route(
"/.well-known/openid-configuration",
get(move || {
let token_endpoint = token_endpoint.clone();
async move {
axum::Json(serde_json::json!({
"token_endpoint": token_endpoint
}))
}
}),
)
.route(
"/token",
post(|| async {
axum::Json(serde_json::json!({
"access_token": "fresh-access",
"refresh_token": "fresh-refresh",
"expires_in": 3600
}))
}),
);
let _server = tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
tokio::task::yield_now().await;
let ok_before = crate::metrics::oidc_refresh_count(OidcRefreshOutcome::Ok);
let duration_before = crate::metrics::oidc_refresh_duration_sample_count();
let provider = OidcAuthProviderBuilder::new("stale-access", "refresh-tok", base, "client1")
.expires_at(Utc::now() - chrono::Duration::hours(1))
.build();
// try_refresh uses block_in_place; needs multi-thread runtime.
let cred = tokio::task::spawn_blocking(move || provider.current())
.await
.expect("join");
match cred {
AuthCredential::Bearer { token } => assert_eq!(token, "fresh-access"),
_ => panic!("expected Bearer"),
}
assert_eq!(
crate::metrics::oidc_refresh_count(OidcRefreshOutcome::Ok),
ok_before + 1
);
assert_eq!(
crate::metrics::oidc_refresh_duration_sample_count(),
duration_before + 1
);
}
#[test]
fn identity_surfaces_principal_fields() {
let provider = OidcAuthProviderBuilder::new("tok", "rt", "https://auth.example.com", "c1")