Add renewable EMA HTTP authentication and credential versioning (#49987)

## What changed

- Add `EmaAuthenticatedHttpClient` to cache and renew resource bearers, serialize concurrent renewals, and include authentication time in the request deadline. Confine bearers to the configured MCP endpoint and reject redirects.
- Return authentication-required errors for `401` responses without replaying MCP operations. Preserve a newer cached bearer when a delayed response rejects an older one.
- Add revocable request authority that cancels pending requests and ends response streams.
- Pin enterprise credentials to a persistent version advanced on save or deletion. Use short credential locks and invalidate rejected grants only when their record and version are still current, preserving newer logins.
- Hide keyring account identifiers from enterprise credential errors and traces while preserving authentication failure classification.

## Testing

Add tests for token expiry and renewal, endpoint confinement, redirects, request deadlines, revocation during renewal and streaming, delayed `401` responses, credential replacement and logout races, and credential error privacy.

GitOrigin-RevId: ee3e5cf7ecbba3d09e1ab29ed4fbb2a35eacc60d
This commit is contained in:
Nick Steele
2026-10-01 13:57:48 +00:00
committed by copyberry
parent a933dd77db
commit ecc78e4cf5
23 changed files with 2109 additions and 61 deletions
@@ -43,6 +43,7 @@ impl ExecServerError {
| Self::Json(_)
| Self::HttpRequest(_)
| Self::Protocol(_)
| Self::AuthenticationRequired(_)
| Self::ProvisioningModeConflict { .. }
| Self::Server { .. }
| Self::EnvironmentRegistryHttp { .. }
@@ -19,6 +19,7 @@ use tokio::runtime::Handle;
use tokio::sync::OwnedSemaphorePermit;
use tokio::sync::mpsc;
use tokio::sync::mpsc::error::TrySendError;
use tokio_util::sync::CancellationToken;
use tracing::debug;
use crate::client::ExecServerError;
@@ -75,6 +76,7 @@ enum HttpResponseBodyStreamInner {
/// id until EOF or a terminal error.
pub struct HttpResponseBodyStream {
inner: HttpResponseBodyStreamInner,
cancellation: Option<CancellationToken>,
}
impl HttpResponseBodyStream {
@@ -93,6 +95,7 @@ impl HttpResponseBodyStream {
inner: HttpResponseBodyStreamInner::Local {
body: Box::pin(body),
},
cancellation: None,
}
}
@@ -101,6 +104,7 @@ impl HttpResponseBodyStream {
inner: HttpResponseBodyStreamInner::Local {
body: Box::pin(response.bytes_stream()),
},
cancellation: None,
}
}
@@ -118,14 +122,32 @@ impl HttpResponseBodyStream {
pending_eof: false,
closed: false,
},
cancellation: None,
}
}
/// Ends this stream when its owning request authority is revoked.
pub fn cancel_on(mut self, cancellation: CancellationToken) -> Self {
self.cancellation = Some(cancellation);
self
}
/// Receives the next response-body chunk.
///
/// Returns `Ok(None)` at EOF and converts sequence gaps or stream-side
/// stream errors into protocol errors.
pub async fn recv(&mut self) -> Result<Option<Vec<u8>>, ExecServerError> {
let Some(cancellation) = self.cancellation.clone() else {
return self.recv_inner().await;
};
tokio::select! {
biased;
() = cancellation.cancelled() => Ok(None),
result = self.recv_inner() => result,
}
}
async fn recv_inner(&mut self) -> Result<Option<Vec<u8>>, ExecServerError> {
match &mut self.inner {
HttpResponseBodyStreamInner::Local { body } => match body.next().await {
Some(chunk) => match chunk {