Add explicit gateway OAuth sign-in to app-server (#47207)

## Why

Clients with a gateway sign-in UI need to control when browser authorization starts and inspect credential readiness before making authenticated requests.

## What changed

- Add `initialize.capabilities.explicitGatewayOauth` to require explicit login while allowing existing credentials to refresh. Preserve automatic authorization for clients that omit the capability, and prevent later connections from undoing explicit opt-in.
- Add `account/gatewayOAuth/read`, `account/gatewayOAuth/login`, and `account/gatewayOAuth/cancel`, plus `account/gatewayOAuth/changed` notifications. Send the authorization URL only to the initiating connection and cancel its login on disconnect.
- Check gateway authentication before returning cached results from `model/list`, with restart guidance when provider settings have changed.
- Update protocol schemas, TypeScript and Python bindings, and document capability probing and the sign-in lifecycle.

## Testing

Add tests for passive readiness reads, credential reuse after login, token exchange failures, cancellation and immediate retry, connection ownership, legacy versus explicit login, and model-list authentication checks.

GitOrigin-RevId: a26e116a62c9f3db84dc238a40b7f4e816eb1594
This commit is contained in:
alexsong-oai
2026-09-22 06:36:09 +00:00
committed by copyberry
parent 6ea62c4396
commit 064e701b0f
54 changed files with 2134 additions and 22 deletions
@@ -590,6 +590,62 @@ pub struct AccountUpdatedNotification {
pub plan_type: Option<PlanType>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/", rename_all = "camelCase")]
pub enum GatewayOAuthStatus {
NotReady,
Started,
Succeeded,
Failed,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct GatewayOAuthChangedNotification {
/// Authorization handoff, sent only to the connection that started login.
pub auth_url: Option<String>,
pub provider_id: String,
pub status: GatewayOAuthStatus,
pub error: Option<String>,
}
impl std::fmt::Debug for GatewayOAuthChangedNotification {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GatewayOAuthChangedNotification")
.field("auth_url", &self.auth_url.as_ref().map(|_| "[REDACTED]"))
.field("provider_id", &self.provider_id)
.field("status", &self.status)
.field("error", &self.error)
.finish()
}
}
/// Current effective gateway policy and credential readiness; never contains credentials.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct GatewayOAuthReadResponse {
pub provider_id: String,
pub provider_name: String,
/// Whether the selected provider uses gateway OAuth, even when already signed in.
pub required: bool,
/// Null when the effective provider does not use gateway OAuth.
pub status: Option<GatewayOAuthStatus>,
pub error: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct GatewayOAuthLoginResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct GatewayOAuthCancelResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]