diff --git a/codex-rs/analytics/src/tests/suite/approvals_tests.rs b/codex-rs/analytics/src/tests/suite/approvals_tests.rs index 97a84b723a..22a56c03de 100644 --- a/codex-rs/analytics/src/tests/suite/approvals_tests.rs +++ b/codex-rs/analytics/src/tests/suite/approvals_tests.rs @@ -256,6 +256,7 @@ async fn guardian_review_event_ingests_custom_fact_with_optional_target_item() { version: "1.0.0".to_string(), }, capabilities: Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: false, request_attestation: false, opt_out_notification_methods: None, diff --git a/codex-rs/analytics/src/tests/suite/threads_tests.rs b/codex-rs/analytics/src/tests/suite/threads_tests.rs index 546bf65478..ac9f9c0262 100644 --- a/codex-rs/analytics/src/tests/suite/threads_tests.rs +++ b/codex-rs/analytics/src/tests/suite/threads_tests.rs @@ -217,6 +217,7 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize version: "1.0.0".to_string(), }, capabilities: Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: false, request_attestation: false, opt_out_notification_methods: None, diff --git a/codex-rs/analytics/src/tests/suite/turns/compaction_tests.rs b/codex-rs/analytics/src/tests/suite/turns/compaction_tests.rs index 86373e2e9a..68d20000b5 100644 --- a/codex-rs/analytics/src/tests/suite/turns/compaction_tests.rs +++ b/codex-rs/analytics/src/tests/suite/turns/compaction_tests.rs @@ -139,6 +139,7 @@ async fn compaction_event_ingests_custom_fact() { version: "1.0.0".to_string(), }, capabilities: Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: false, request_attestation: false, opt_out_notification_methods: None, diff --git a/codex-rs/analytics/src/tests/support.rs b/codex-rs/analytics/src/tests/support.rs index 995d302d13..4386962b6a 100644 --- a/codex-rs/analytics/src/tests/support.rs +++ b/codex-rs/analytics/src/tests/support.rs @@ -585,6 +585,7 @@ pub(super) fn sample_initialize_fact(connection_id: u64) -> AnalyticsFact { version: "1.0.0".to_string(), }, capabilities: Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: false, request_attestation: false, opt_out_notification_methods: None, diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index 588e5b1566..1e33cee0b0 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -214,6 +214,7 @@ impl InProcessClientStartArgs { /// Builds initialize params from caller-provided metadata. pub fn initialize_params(&self) -> InitializeParams { let capabilities = InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: self.experimental_api, request_attestation: false, extensions: None, diff --git a/codex-rs/app-server-client/src/remote.rs b/codex-rs/app-server-client/src/remote.rs index 8a31b266b4..78d6272d12 100644 --- a/codex-rs/app-server-client/src/remote.rs +++ b/codex-rs/app-server-client/src/remote.rs @@ -92,6 +92,7 @@ pub struct RemoteAppServerConnectArgs { impl RemoteAppServerConnectArgs { pub(crate) fn initialize_params(&self) -> InitializeParams { let capabilities = InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: self.experimental_api, request_attestation: false, extensions: None, diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 9ac17b8a03..8b1f91ed5e 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1634,6 +1634,10 @@ "description": "Opt into receiving experimental API methods and fields.", "type": "boolean" }, + "explicitGatewayOauth": { + "description": "Use explicit gateway OAuth login instead of automatic browser authorization. Applies to this app-server's gateway runtime; later connections cannot undo it.", + "type": "boolean" + }, "extensions": { "additionalProperties": true, "description": "MCP extension settings declared by the app-server client.", @@ -7504,6 +7508,75 @@ "title": "Model/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/gatewayOAuth/read" + ], + "title": "Account/gatewayOAuth/readRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Account/gatewayOAuth/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/gatewayOAuth/login" + ], + "title": "Account/gatewayOAuth/loginRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Account/gatewayOAuth/loginRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/gatewayOAuth/cancel" + ], + "title": "Account/gatewayOAuth/cancelRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Account/gatewayOAuth/cancelRequest", + "type": "object" + }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 70eb9c821c..dc0cab3d95 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -1933,6 +1933,43 @@ ], "type": "object" }, + "GatewayOAuthChangedNotification": { + "properties": { + "authUrl": { + "description": "Authorization handoff, sent only to the connection that started login.", + "type": [ + "string", + "null" + ] + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "providerId": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/GatewayOAuthStatus" + } + }, + "required": [ + "providerId", + "status" + ], + "type": "object" + }, + "GatewayOAuthStatus": { + "enum": [ + "notReady", + "started", + "succeeded", + "failed" + ], + "type": "string" + }, "GitInfo": { "properties": { "branch": { @@ -7791,6 +7828,26 @@ "title": "Account/updatedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "account/gatewayOAuth/changed" + ], + "title": "Account/gatewayOAuth/changedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/GatewayOAuthChangedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Account/gatewayOAuth/changedNotification", + "type": "object" + }, { "properties": { "method": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index c3c8a756b9..f47d4929f6 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -1813,6 +1813,75 @@ "title": "Model/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "account/gatewayOAuth/read" + ], + "title": "Account/gatewayOAuth/readRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Account/gatewayOAuth/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "account/gatewayOAuth/login" + ], + "title": "Account/gatewayOAuth/loginRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Account/gatewayOAuth/loginRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "account/gatewayOAuth/cancel" + ], + "title": "Account/gatewayOAuth/cancelRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Account/gatewayOAuth/cancelRequest", + "type": "object" + }, { "properties": { "id": { @@ -3290,6 +3359,10 @@ "description": "Opt into receiving experimental API methods and fields.", "type": "boolean" }, + "explicitGatewayOauth": { + "description": "Use explicit gateway OAuth login instead of automatic browser authorization. Applies to this app-server's gateway runtime; later connections cannot undo it.", + "type": "boolean" + }, "extensions": { "additionalProperties": true, "description": "MCP extension settings declared by the app-server client.", @@ -5451,6 +5524,26 @@ "title": "Account/updatedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "account/gatewayOAuth/changed" + ], + "title": "Account/gatewayOAuth/changedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/GatewayOAuthChangedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Account/gatewayOAuth/changedNotification", + "type": "object" + }, { "properties": { "method": { @@ -12452,6 +12545,95 @@ } ] }, + "GatewayOAuthCancelResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GatewayOAuthCancelResponse", + "type": "object" + }, + "GatewayOAuthChangedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "authUrl": { + "description": "Authorization handoff, sent only to the connection that started login.", + "type": [ + "string", + "null" + ] + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "providerId": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/v2/GatewayOAuthStatus" + } + }, + "required": [ + "providerId", + "status" + ], + "title": "GatewayOAuthChangedNotification", + "type": "object" + }, + "GatewayOAuthLoginResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GatewayOAuthLoginResponse", + "type": "object" + }, + "GatewayOAuthReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Current effective gateway policy and credential readiness; never contains credentials.", + "properties": { + "error": { + "type": [ + "string", + "null" + ] + }, + "providerId": { + "type": "string" + }, + "providerName": { + "type": "string" + }, + "required": { + "description": "Whether the selected provider uses gateway OAuth, even when already signed in.", + "type": "boolean" + }, + "status": { + "anyOf": [ + { + "$ref": "#/definitions/v2/GatewayOAuthStatus" + }, + { + "type": "null" + } + ], + "description": "Null when the effective provider does not use gateway OAuth." + } + }, + "required": [ + "providerId", + "providerName", + "required" + ], + "title": "GatewayOAuthReadResponse", + "type": "object" + }, + "GatewayOAuthStatus": { + "enum": [ + "notReady", + "started", + "succeeded", + "failed" + ], + "type": "string" + }, "GetAccountParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 76f4348dd8..ea3d7fca29 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -3362,6 +3362,75 @@ "title": "Model/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/gatewayOAuth/read" + ], + "title": "Account/gatewayOAuth/readRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Account/gatewayOAuth/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/gatewayOAuth/login" + ], + "title": "Account/gatewayOAuth/loginRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Account/gatewayOAuth/loginRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/gatewayOAuth/cancel" + ], + "title": "Account/gatewayOAuth/cancelRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Account/gatewayOAuth/cancelRequest", + "type": "object" + }, { "properties": { "id": { @@ -8443,6 +8512,95 @@ "title": "FuzzyFileSearchSessionUpdatedNotification", "type": "object" }, + "GatewayOAuthCancelResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GatewayOAuthCancelResponse", + "type": "object" + }, + "GatewayOAuthChangedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "authUrl": { + "description": "Authorization handoff, sent only to the connection that started login.", + "type": [ + "string", + "null" + ] + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "providerId": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/GatewayOAuthStatus" + } + }, + "required": [ + "providerId", + "status" + ], + "title": "GatewayOAuthChangedNotification", + "type": "object" + }, + "GatewayOAuthLoginResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GatewayOAuthLoginResponse", + "type": "object" + }, + "GatewayOAuthReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Current effective gateway policy and credential readiness; never contains credentials.", + "properties": { + "error": { + "type": [ + "string", + "null" + ] + }, + "providerId": { + "type": "string" + }, + "providerName": { + "type": "string" + }, + "required": { + "description": "Whether the selected provider uses gateway OAuth, even when already signed in.", + "type": "boolean" + }, + "status": { + "anyOf": [ + { + "$ref": "#/definitions/GatewayOAuthStatus" + }, + { + "type": "null" + } + ], + "description": "Null when the effective provider does not use gateway OAuth." + } + }, + "required": [ + "providerId", + "providerName", + "required" + ], + "title": "GatewayOAuthReadResponse", + "type": "object" + }, + "GatewayOAuthStatus": { + "enum": [ + "notReady", + "started", + "succeeded", + "failed" + ], + "type": "string" + }, "GetAccountParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -9502,6 +9660,10 @@ "description": "Opt into receiving experimental API methods and fields.", "type": "boolean" }, + "explicitGatewayOauth": { + "description": "Use explicit gateway OAuth login instead of automatic browser authorization. Applies to this app-server's gateway runtime; later connections cannot undo it.", + "type": "boolean" + }, "extensions": { "additionalProperties": true, "description": "MCP extension settings declared by the app-server client.", @@ -16749,6 +16911,26 @@ "title": "Account/updatedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "account/gatewayOAuth/changed" + ], + "title": "Account/gatewayOAuth/changedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/GatewayOAuthChangedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Account/gatewayOAuth/changedNotification", + "type": "object" + }, { "properties": { "method": { diff --git a/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json b/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json index 7acc76386c..750dddeaf5 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json @@ -30,6 +30,10 @@ "description": "Opt into receiving experimental API methods and fields.", "type": "boolean" }, + "explicitGatewayOauth": { + "description": "Use explicit gateway OAuth login instead of automatic browser authorization. Applies to this app-server's gateway runtime; later connections cannot undo it.", + "type": "boolean" + }, "extensions": { "additionalProperties": true, "description": "MCP extension settings declared by the app-server client.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/GatewayOAuthCancelResponse.json b/codex-rs/app-server-protocol/schema/json/v2/GatewayOAuthCancelResponse.json new file mode 100644 index 0000000000..283580b1df --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/GatewayOAuthCancelResponse.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GatewayOAuthCancelResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/GatewayOAuthChangedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/GatewayOAuthChangedNotification.json new file mode 100644 index 0000000000..0730c19889 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/GatewayOAuthChangedNotification.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "GatewayOAuthStatus": { + "enum": [ + "notReady", + "started", + "succeeded", + "failed" + ], + "type": "string" + } + }, + "properties": { + "authUrl": { + "description": "Authorization handoff, sent only to the connection that started login.", + "type": [ + "string", + "null" + ] + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "providerId": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/GatewayOAuthStatus" + } + }, + "required": [ + "providerId", + "status" + ], + "title": "GatewayOAuthChangedNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/GatewayOAuthLoginResponse.json b/codex-rs/app-server-protocol/schema/json/v2/GatewayOAuthLoginResponse.json new file mode 100644 index 0000000000..bb11dc15a7 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/GatewayOAuthLoginResponse.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GatewayOAuthLoginResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/GatewayOAuthReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/GatewayOAuthReadResponse.json new file mode 100644 index 0000000000..98340d2c7b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/GatewayOAuthReadResponse.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "GatewayOAuthStatus": { + "enum": [ + "notReady", + "started", + "succeeded", + "failed" + ], + "type": "string" + } + }, + "description": "Current effective gateway policy and credential readiness; never contains credentials.", + "properties": { + "error": { + "type": [ + "string", + "null" + ] + }, + "providerId": { + "type": "string" + }, + "providerName": { + "type": "string" + }, + "required": { + "description": "Whether the selected provider uses gateway OAuth, even when already signed in.", + "type": "boolean" + }, + "status": { + "anyOf": [ + { + "$ref": "#/definitions/GatewayOAuthStatus" + }, + { + "type": "null" + } + ], + "description": "Null when the effective provider does not use gateway OAuth." + } + }, + "required": [ + "providerId", + "providerName", + "required" + ], + "title": "GatewayOAuthReadResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 01951041d8..9df82d48a8 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 21284ac61f..5254e71a76 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 45ed6aa96c..5e3fb8c915 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -104,4 +104,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/delete", id: RequestId, params: ThreadDeleteParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/attachment/add", id: RequestId, params: ThreadAttachmentAddParams, } | { "method": "thread/attachment/list", id: RequestId, params: ThreadAttachmentListParams, } | { "method": "thread/attachment/remove", id: RequestId, params: ThreadAttachmentRemoveParams, } | { "method": "thread/section/move", id: RequestId, params: ThreadSectionMoveParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/revert", id: RequestId, params: ThreadRevertParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "threadSection/list", id: RequestId, params: ThreadSectionListParams, } | { "method": "threadSection/create", id: RequestId, params: ThreadSectionCreateParams, } | { "method": "threadSection/update", id: RequestId, params: ThreadSectionUpdateParams, } | { "method": "threadSection/delete", id: RequestId, params: ThreadSectionDeleteParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/turns/list", id: RequestId, params: ThreadTurnsListParams, } | { "method": "thread/items/list", id: RequestId, params: ThreadItemsListParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/reconcile", id: RequestId, params: PluginReconcileParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/read", id: RequestId, params: AppsReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "app/installed", id: RequestId, params: AppsInstalledParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params?: GetAccountRateLimitsParams | undefined, } | { "method": "account/rateLimitResetCredit/consume", id: RequestId, params: ConsumeAccountRateLimitResetCreditParams, } | { "method": "account/usage/read", id: RequestId, params?: GetAccountTokenUsageParams | undefined, } | { "method": "account/workspaceMessages/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "externalAgentConfig/import/recordHistory", id: RequestId, params: ExternalAgentConfigImportHistoryRecordParams, } | { "method": "externalAgentConfig/import/readHistories", id: RequestId, params: undefined, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/delete", id: RequestId, params: ThreadDeleteParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/attachment/add", id: RequestId, params: ThreadAttachmentAddParams, } | { "method": "thread/attachment/list", id: RequestId, params: ThreadAttachmentListParams, } | { "method": "thread/attachment/remove", id: RequestId, params: ThreadAttachmentRemoveParams, } | { "method": "thread/section/move", id: RequestId, params: ThreadSectionMoveParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/revert", id: RequestId, params: ThreadRevertParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "threadSection/list", id: RequestId, params: ThreadSectionListParams, } | { "method": "threadSection/create", id: RequestId, params: ThreadSectionCreateParams, } | { "method": "threadSection/update", id: RequestId, params: ThreadSectionUpdateParams, } | { "method": "threadSection/delete", id: RequestId, params: ThreadSectionDeleteParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/turns/list", id: RequestId, params: ThreadTurnsListParams, } | { "method": "thread/items/list", id: RequestId, params: ThreadItemsListParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/reconcile", id: RequestId, params: PluginReconcileParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/read", id: RequestId, params: AppsReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "app/installed", id: RequestId, params: AppsInstalledParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "account/gatewayOAuth/read", id: RequestId, params: undefined, } | { "method": "account/gatewayOAuth/login", id: RequestId, params: undefined, } | { "method": "account/gatewayOAuth/cancel", id: RequestId, params: undefined, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params?: GetAccountRateLimitsParams | undefined, } | { "method": "account/rateLimitResetCredit/consume", id: RequestId, params: ConsumeAccountRateLimitResetCreditParams, } | { "method": "account/usage/read", id: RequestId, params?: GetAccountTokenUsageParams | undefined, } | { "method": "account/workspaceMessages/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "externalAgentConfig/import/recordHistory", id: RequestId, params: ExternalAgentConfigImportHistoryRecordParams, } | { "method": "externalAgentConfig/import/readHistories", id: RequestId, params: undefined, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts b/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts index 6b4afd166b..ac82d743e3 100644 --- a/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts +++ b/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts @@ -7,6 +7,11 @@ import type { JsonValue } from "./serde_json/JsonValue"; * Client-declared capabilities negotiated during initialize. */ export type InitializeCapabilities = { +/** + * Use explicit gateway OAuth login instead of automatic browser authorization. + * Applies to this app-server's gateway runtime; later connections cannot undo it. + */ +explicitGatewayOauth?: boolean, /** * Opt into receiving experimental API methods and fields. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts index da1360df17..360f971862 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts @@ -21,6 +21,7 @@ import type { ExternalAgentConfigImportProgressNotification } from "./v2/Externa import type { FileChangeOutputDeltaNotification } from "./v2/FileChangeOutputDeltaNotification"; import type { FileChangePatchUpdatedNotification } from "./v2/FileChangePatchUpdatedNotification"; import type { FsChangedNotification } from "./v2/FsChangedNotification"; +import type { GatewayOAuthChangedNotification } from "./v2/GatewayOAuthChangedNotification"; import type { GuardianWarningNotification } from "./v2/GuardianWarningNotification"; import type { HookCompletedNotification } from "./v2/HookCompletedNotification"; import type { HookStartedNotification } from "./v2/HookStartedNotification"; @@ -87,4 +88,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW /** * Notification sent from the server to the client. */ -export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/deleted", "params": ThreadDeletedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "thread/reverted", "params": ThreadRevertedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/attachment/updated", "params": ThreadAttachmentUpdatedNotification } | { "method": "thread/goal/updated", "params": ThreadGoalUpdatedNotification } | { "method": "thread/goal/cleared", "params": ThreadGoalClearedNotification } | { "method": "thread/queue/changed", "params": ThreadQueueChangedNotification } | { "method": "project/changed", "params": ProjectChangedNotification } | { "method": "thread/project/updated", "params": ThreadProjectUpdatedNotification } | { "method": "thread/environment/connected", "params": EnvironmentConnectionNotification } | { "method": "thread/environment/disconnected", "params": EnvironmentConnectionNotification } | { "method": "thread/settings/updated", "params": ThreadSettingsUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "autoApprovalReview/strictReviewRequired", "params": StrictReviewRequiredNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "rawResponse/completed", "params": RawResponseCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "process/outputDelta", "params": ProcessOutputDeltaNotification } | { "method": "process/exited", "params": ProcessExitedNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "mcpServer/event/stream/notification", "params": McpServerEventStreamNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "remoteControl/status/changed", "params": RemoteControlStatusChangedNotification } | { "method": "externalAgentConfig/import/progress", "params": ExternalAgentConfigImportProgressNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "model/verification", "params": ModelVerificationNotification } | { "method": "modelProvider/authRecoveryStarted", "params": AuthRecoveryNotification } | { "method": "modelProvider/authRecoveryCompleted", "params": AuthRecoveryNotification } | { "method": "turn/moderationMetadata", "params": TurnModerationMetadataNotification } | { "method": "model/safetyBuffering/updated", "params": ModelSafetyBufferingUpdatedNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/item/started", "params": ThreadRealtimeItemStartedNotification } | { "method": "thread/realtime/item/transcript/delta", "params": ThreadRealtimeItemTranscriptDeltaNotification } | { "method": "thread/realtime/item/completed", "params": ThreadRealtimeItemCompletedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }; +export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/deleted", "params": ThreadDeletedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "thread/reverted", "params": ThreadRevertedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/attachment/updated", "params": ThreadAttachmentUpdatedNotification } | { "method": "thread/goal/updated", "params": ThreadGoalUpdatedNotification } | { "method": "thread/goal/cleared", "params": ThreadGoalClearedNotification } | { "method": "thread/queue/changed", "params": ThreadQueueChangedNotification } | { "method": "project/changed", "params": ProjectChangedNotification } | { "method": "thread/project/updated", "params": ThreadProjectUpdatedNotification } | { "method": "thread/environment/connected", "params": EnvironmentConnectionNotification } | { "method": "thread/environment/disconnected", "params": EnvironmentConnectionNotification } | { "method": "thread/settings/updated", "params": ThreadSettingsUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "autoApprovalReview/strictReviewRequired", "params": StrictReviewRequiredNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "rawResponse/completed", "params": RawResponseCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "process/outputDelta", "params": ProcessOutputDeltaNotification } | { "method": "process/exited", "params": ProcessExitedNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "mcpServer/event/stream/notification", "params": McpServerEventStreamNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/gatewayOAuth/changed", "params": GatewayOAuthChangedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "remoteControl/status/changed", "params": RemoteControlStatusChangedNotification } | { "method": "externalAgentConfig/import/progress", "params": ExternalAgentConfigImportProgressNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "model/verification", "params": ModelVerificationNotification } | { "method": "modelProvider/authRecoveryStarted", "params": AuthRecoveryNotification } | { "method": "modelProvider/authRecoveryCompleted", "params": AuthRecoveryNotification } | { "method": "turn/moderationMetadata", "params": TurnModerationMetadataNotification } | { "method": "model/safetyBuffering/updated", "params": ModelSafetyBufferingUpdatedNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/item/started", "params": ThreadRealtimeItemStartedNotification } | { "method": "thread/realtime/item/transcript/delta", "params": ThreadRealtimeItemTranscriptDeltaNotification } | { "method": "thread/realtime/item/completed", "params": ThreadRealtimeItemCompletedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerNotificationEnvelope.ts b/codex-rs/app-server-protocol/schema/typescript/ServerNotificationEnvelope.ts index 47f35db0c7..3c306f005f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerNotificationEnvelope.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerNotificationEnvelope.ts @@ -21,6 +21,7 @@ import type { ExternalAgentConfigImportProgressNotification } from "./v2/Externa import type { FileChangeOutputDeltaNotification } from "./v2/FileChangeOutputDeltaNotification"; import type { FileChangePatchUpdatedNotification } from "./v2/FileChangePatchUpdatedNotification"; import type { FsChangedNotification } from "./v2/FsChangedNotification"; +import type { GatewayOAuthChangedNotification } from "./v2/GatewayOAuthChangedNotification"; import type { GuardianWarningNotification } from "./v2/GuardianWarningNotification"; import type { HookCompletedNotification } from "./v2/HookCompletedNotification"; import type { HookStartedNotification } from "./v2/HookStartedNotification"; @@ -97,4 +98,4 @@ export type ServerNotificationEnvelope = { * Optional so clients can decode notifications from older app-server * versions. Current app-server versions always populate it. */ -emittedAtMs?: number, } & ({ "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/deleted", "params": ThreadDeletedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "thread/reverted", "params": ThreadRevertedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/attachment/updated", "params": ThreadAttachmentUpdatedNotification } | { "method": "thread/goal/updated", "params": ThreadGoalUpdatedNotification } | { "method": "thread/goal/cleared", "params": ThreadGoalClearedNotification } | { "method": "thread/queue/changed", "params": ThreadQueueChangedNotification } | { "method": "project/changed", "params": ProjectChangedNotification } | { "method": "thread/project/updated", "params": ThreadProjectUpdatedNotification } | { "method": "thread/environment/connected", "params": EnvironmentConnectionNotification } | { "method": "thread/environment/disconnected", "params": EnvironmentConnectionNotification } | { "method": "thread/settings/updated", "params": ThreadSettingsUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "autoApprovalReview/strictReviewRequired", "params": StrictReviewRequiredNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "rawResponse/completed", "params": RawResponseCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "process/outputDelta", "params": ProcessOutputDeltaNotification } | { "method": "process/exited", "params": ProcessExitedNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "mcpServer/event/stream/notification", "params": McpServerEventStreamNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "remoteControl/status/changed", "params": RemoteControlStatusChangedNotification } | { "method": "externalAgentConfig/import/progress", "params": ExternalAgentConfigImportProgressNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "model/verification", "params": ModelVerificationNotification } | { "method": "modelProvider/authRecoveryStarted", "params": AuthRecoveryNotification } | { "method": "modelProvider/authRecoveryCompleted", "params": AuthRecoveryNotification } | { "method": "turn/moderationMetadata", "params": TurnModerationMetadataNotification } | { "method": "model/safetyBuffering/updated", "params": ModelSafetyBufferingUpdatedNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/item/started", "params": ThreadRealtimeItemStartedNotification } | { "method": "thread/realtime/item/transcript/delta", "params": ThreadRealtimeItemTranscriptDeltaNotification } | { "method": "thread/realtime/item/completed", "params": ThreadRealtimeItemCompletedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }); +emittedAtMs?: number, } & ({ "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/deleted", "params": ThreadDeletedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "thread/reverted", "params": ThreadRevertedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/attachment/updated", "params": ThreadAttachmentUpdatedNotification } | { "method": "thread/goal/updated", "params": ThreadGoalUpdatedNotification } | { "method": "thread/goal/cleared", "params": ThreadGoalClearedNotification } | { "method": "thread/queue/changed", "params": ThreadQueueChangedNotification } | { "method": "project/changed", "params": ProjectChangedNotification } | { "method": "thread/project/updated", "params": ThreadProjectUpdatedNotification } | { "method": "thread/environment/connected", "params": EnvironmentConnectionNotification } | { "method": "thread/environment/disconnected", "params": EnvironmentConnectionNotification } | { "method": "thread/settings/updated", "params": ThreadSettingsUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "autoApprovalReview/strictReviewRequired", "params": StrictReviewRequiredNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "rawResponse/completed", "params": RawResponseCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "process/outputDelta", "params": ProcessOutputDeltaNotification } | { "method": "process/exited", "params": ProcessExitedNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "mcpServer/event/stream/notification", "params": McpServerEventStreamNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/gatewayOAuth/changed", "params": GatewayOAuthChangedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "remoteControl/status/changed", "params": RemoteControlStatusChangedNotification } | { "method": "externalAgentConfig/import/progress", "params": ExternalAgentConfigImportProgressNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "model/verification", "params": ModelVerificationNotification } | { "method": "modelProvider/authRecoveryStarted", "params": AuthRecoveryNotification } | { "method": "modelProvider/authRecoveryCompleted", "params": AuthRecoveryNotification } | { "method": "turn/moderationMetadata", "params": TurnModerationMetadataNotification } | { "method": "model/safetyBuffering/updated", "params": ModelSafetyBufferingUpdatedNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/item/started", "params": ThreadRealtimeItemStartedNotification } | { "method": "thread/realtime/item/transcript/delta", "params": ThreadRealtimeItemTranscriptDeltaNotification } | { "method": "thread/realtime/item/completed", "params": ThreadRealtimeItemCompletedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }); diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthCancelResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthCancelResponse.ts new file mode 100644 index 0000000000..957542bf56 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthCancelResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GatewayOAuthCancelResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthChangedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthChangedNotification.ts new file mode 100644 index 0000000000..6e88ea2542 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthChangedNotification.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { GatewayOAuthStatus } from "./GatewayOAuthStatus"; + +export type GatewayOAuthChangedNotification = { +/** + * Authorization handoff, sent only to the connection that started login. + */ +authUrl: string | null, providerId: string, status: GatewayOAuthStatus, error: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthLoginResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthLoginResponse.ts new file mode 100644 index 0000000000..a171d58303 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthLoginResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GatewayOAuthLoginResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthReadResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthReadResponse.ts new file mode 100644 index 0000000000..78b53398d5 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthReadResponse.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { GatewayOAuthStatus } from "./GatewayOAuthStatus"; + +/** + * Current effective gateway policy and credential readiness; never contains credentials. + */ +export type GatewayOAuthReadResponse = { providerId: string, providerName: string, +/** + * Whether the selected provider uses gateway OAuth, even when already signed in. + */ +required: boolean, +/** + * Null when the effective provider does not use gateway OAuth. + */ +status: GatewayOAuthStatus | null, error: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthStatus.ts new file mode 100644 index 0000000000..2423ed04ac --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GatewayOAuthStatus = "notReady" | "started" | "succeeded" | "failed"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 64c7420cc0..514ae8de84 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -194,6 +194,11 @@ export type { FsWatchParams } from "./FsWatchParams"; export type { FsWatchResponse } from "./FsWatchResponse"; export type { FsWriteFileParams } from "./FsWriteFileParams"; export type { FsWriteFileResponse } from "./FsWriteFileResponse"; +export type { GatewayOAuthCancelResponse } from "./GatewayOAuthCancelResponse"; +export type { GatewayOAuthChangedNotification } from "./GatewayOAuthChangedNotification"; +export type { GatewayOAuthLoginResponse } from "./GatewayOAuthLoginResponse"; +export type { GatewayOAuthReadResponse } from "./GatewayOAuthReadResponse"; +export type { GatewayOAuthStatus } from "./GatewayOAuthStatus"; export type { GetAccountParams } from "./GetAccountParams"; export type { GetAccountRateLimitsParams } from "./GetAccountRateLimitsParams"; export type { GetAccountRateLimitsResponse } from "./GetAccountRateLimitsResponse"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index f2e8620408..8bd7d25da4 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1105,6 +1105,21 @@ client_request_definitions! { serialization: None, response: v2::ModelListResponse, }, + GatewayOAuthRead => "account/gatewayOAuth/read" { + params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, + serialization: None, + response: v2::GatewayOAuthReadResponse, + }, + GatewayOAuthLogin => "account/gatewayOAuth/login" { + params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, + serialization: None, + response: v2::GatewayOAuthLoginResponse, + }, + GatewayOAuthCancel => "account/gatewayOAuth/cancel" { + params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, + serialization: None, + response: v2::GatewayOAuthCancelResponse, + }, ModelProviderCapabilitiesRead => "modelProvider/capabilities/read" { params: v2::ModelProviderCapabilitiesReadParams, serialization: None, @@ -1965,6 +1980,7 @@ server_notification_definitions! { #[experimental("mcpServer/event/stream/notification")] McpServerEventStream => "mcpServer/event/stream/notification" (v2::McpServerEventStreamNotification), AccountUpdated => "account/updated" (v2::AccountUpdatedNotification), + GatewayOAuthChanged => "account/gatewayOAuth/changed" (v2::GatewayOAuthChangedNotification), AccountRateLimitsUpdated => "account/rateLimits/updated" (v2::AccountRateLimitsUpdatedNotification), AppListUpdated => "app/list/updated" (v2::AppListUpdatedNotification), RemoteControlStatusChanged => "remoteControl/status/changed" (v2::RemoteControlStatusChangedNotification), @@ -2726,6 +2742,7 @@ mod tests { version: "0.1.0".to_string(), }, capabilities: Some(v1::InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: true, request_attestation: true, mcp_server_openai_form_elicitation: true, @@ -2813,6 +2830,7 @@ mod tests { version: "0.1.0".to_string(), }, capabilities: Some(v1::InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: true, request_attestation: true, mcp_server_openai_form_elicitation: true, diff --git a/codex-rs/app-server-protocol/src/protocol/v1.rs b/codex-rs/app-server-protocol/src/protocol/v1.rs index 17013b528f..7d05fb1bd4 100644 --- a/codex-rs/app-server-protocol/src/protocol/v1.rs +++ b/codex-rs/app-server-protocol/src/protocol/v1.rs @@ -44,6 +44,10 @@ pub struct ClientInfo { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct InitializeCapabilities { + /// Use explicit gateway OAuth login instead of automatic browser authorization. + /// Applies to this app-server's gateway runtime; later connections cannot undo it. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub explicit_gateway_oauth: bool, /// Opt into receiving experimental API methods and fields. #[serde(default)] pub experimental_api: bool, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/account.rs b/codex-rs/app-server-protocol/src/protocol/v2/account.rs index 35c2b6eb8a..e1b8e8b2df 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/account.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/account.rs @@ -590,6 +590,62 @@ pub struct AccountUpdatedNotification { pub plan_type: Option, } +#[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, + pub provider_id: String, + pub status: GatewayOAuthStatus, + pub error: Option, +} + +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, + pub error: Option, +} + +#[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/")] diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index 27b8a341fe..4d3c6efa87 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -1752,6 +1752,7 @@ impl CodexClient { version: env!("CARGO_PKG_VERSION").to_string(), }, capabilities: Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api, request_attestation: false, opt_out_notification_methods: Some( diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 5da9f5bdcf..376d4c74bc 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -330,3 +330,65 @@ compatibility but is not emitted. Non-Windows hosts report `notConfigured`. MXC uses the standard `command/exec` streaming and process-control path, including ConPTY when `tty` is enabled. The buffered legacy Windows sandbox restrictions on process control and custom output caps do not apply to MXC. + +### Gateway OAuth sign-in + +Providers configured with `gateway_oauth` require a secondary OAuth credential in +addition to their primary authentication. Clients with a gateway sign-in UI set +`initialize.capabilities.explicitGatewayOauth: true`, complete initialization, and +successfully call `account/gatewayOAuth/read` before sending authenticated requests, +including startup `model/list` and inference requests. Repeat this probe on each +new connection. A successful `initialize` alone does not confirm support: older +servers can ignore the unknown capability and retain automatic browser login. + +Support for `account/gatewayOAuth/read` and `explicitGatewayOauth` is introduced +together, so a successful read confirms support even when `required` is `false` +or `status` is `notReady`. The returned status determines whether sign-in is needed; +it is separate from the capability check. If the probe fails because the method is +unsupported, require a server upgrade. Other errors and timeouts also leave +authenticated requests blocked until a probe succeeds; do not silently fall back +to automatic login. + +With explicit login enabled, app-server refreshes existing credentials, but only the +`account/gatewayOAuth/login` RPC starts browser authorization. Requests needing +sign-in fail promptly so the client can offer that flow. + +Clients that omit the capability or set it to `false`, including the TUI, retain +automatic browser authorization after initialization. Startup credential reads +cannot open a browser before initialization. Explicit opt-in is shared by gateway +managers using the same home and network configuration within the process and +cannot be undone by a later connection that omits the capability. + +- `account/gatewayOAuth/read` returns the current effective `providerId`, + `providerName`, `required`, `status`, and `error`. `required` indicates that this + provider uses gateway OAuth, including when already signed in. This operation + does not refresh tokens or open a browser; `status` is null for other providers. + `notReady` means credentials are not ready. `succeeded` means saved credentials + are locally usable, not that a gateway request has been verified. Reads observe + usable replacement credentials saved by another process sharing the same home. +- `account/gatewayOAuth/login` starts authorization and returns `{}` after + the credential has been saved. Providers requiring OpenAI authentication need a + primary account first. A second login request fails while a login is active. + The initiating connection receives a `started` notification with `authUrl`; the + client must open that URL in a browser that can reach the server callback port. + Other status notifications set `authUrl` to null. Login is rejected if the + initiating connection opted out of `account/gatewayOAuth/changed` notifications. +- `account/gatewayOAuth/cancel` cancels the calling connection's login and returns + `{}` after the active login releases its slot, so the client can immediately + start another login. Closing that connection also cancels its login and releases + the callback listener. Cancellation makes the pending login request fail. +- `account/gatewayOAuth/changed` reports `notReady`, `started`, `succeeded`, or + `failed`, with an optional `error`. Notifications apply to the current effective + gateway configuration. Clients can read readiness when connecting and after + changing configuration. Notifications follow the standard per-connection + `optOutNotificationMethods` setting. + These payloads never contain credentials. + +Read, login, and cancel take no params. Read and login use the app's current +provider, reloading configuration and returning an error if it cannot be loaded. +Status notifications include `providerId`. During browser sign-in, inference +requests fail promptly and can be retried when sign-in succeeds. + +`model/list` also checks gateway authentication before returning cached models. +If authentication fails after the provider configuration changes, it asks the client +to restart Codex so the retained catalog and gateway sign-in use the same provider. diff --git a/codex-rs/app-server/src/gateway_oauth_notifications.rs b/codex-rs/app-server/src/gateway_oauth_notifications.rs new file mode 100644 index 0000000000..91d8d5e43f --- /dev/null +++ b/codex-rs/app-server/src/gateway_oauth_notifications.rs @@ -0,0 +1,65 @@ +//! Forward OAuth progress for the app's current gateway. + +use crate::config_manager::ConfigManager; +use crate::outgoing_message::OutgoingMessageSender; +use codex_app_server_protocol::GatewayOAuthChangedNotification; +use codex_app_server_protocol::GatewayOAuthStatus; +use codex_app_server_protocol::ServerNotification; +use codex_login::AuthManager; +use codex_login::GatewayAuthStatus; +use std::sync::Arc; +use tokio::sync::broadcast::error::RecvError; +use tokio_util::task::AbortOnDropHandle; + +pub(crate) fn status_fields(status: GatewayAuthStatus) -> (GatewayOAuthStatus, Option) { + match status { + GatewayAuthStatus::NotReady => (GatewayOAuthStatus::NotReady, None), + GatewayAuthStatus::Started => (GatewayOAuthStatus::Started, None), + GatewayAuthStatus::Succeeded => (GatewayOAuthStatus::Succeeded, None), + GatewayAuthStatus::Failed { message } => (GatewayOAuthStatus::Failed, Some(message)), + } +} + +pub(crate) fn spawn( + auth_manager: Arc, + config_manager: ConfigManager, + outgoing: Arc, +) -> AbortOnDropHandle<()> { + let mut events = codex_login::subscribe_gateway_auth_status(&auth_manager.runtime_config()); + AbortOnDropHandle::new(tokio::spawn(async move { + loop { + let change = match events.recv().await { + Ok(change) => change, + Err(RecvError::Lagged(_)) => continue, + Err(RecvError::Closed) => break, + }; + let Ok(config) = config_manager + .load_latest_config(/*fallback_cwd*/ None) + .await + else { + continue; + }; + let Ok(Some(manager)) = codex_model_provider::create_model_provider( + config.model_provider.clone(), + Some(Arc::clone(&auth_manager)), + ) + .gateway_auth_manager() else { + continue; + }; + if manager.config() != &change.config { + continue; + } + let (status, error) = status_fields(change.status); + outgoing + .send_server_notification(ServerNotification::GatewayOAuthChanged( + GatewayOAuthChangedNotification { + auth_url: None, + provider_id: config.model_provider_id, + status, + error, + }, + )) + .await; + } + })) +} diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index fc11b838a0..08d3232b23 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -116,6 +116,7 @@ mod external_auth; mod filters; mod fs_watch; mod fuzzy_file_search; +mod gateway_oauth_notifications; mod image_url; pub mod in_process; mod mcp_refresh; diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 118106d710..9e9a90437b 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -290,6 +290,10 @@ impl MessageProcessor { remote_control_handle, plugin_startup_tasks, } = args; + // Startup credential reads must not open a browser before initialize selects the policy. + let gateway_login_control = + codex_login::GatewayLoginControl::for_runtime(&auth_manager.runtime_config()); + gateway_login_control.require_explicit_login(); let thread_state_manager = ThreadStateManager::new(); outgoing.watch_user_verification_auth(Arc::clone(&auth_manager)); // The thread store is intentionally process-scoped. Config reloads can @@ -450,6 +454,7 @@ impl MessageProcessor { ); let git_processor = GitRequestProcessor::new(); let initialize_processor = InitializeRequestProcessor::new( + gateway_login_control, outgoing.clone(), analytics_events_client.clone(), Arc::clone(&config), @@ -862,6 +867,8 @@ impl MessageProcessor { session_state: &ConnectionSessionState, ) { session_state.rpc_gate.close().await; + self.account_processor + .gateway_connection_closed(connection_id); self.request_serialization_queues.discard_closed().await; self.outgoing .disconnect_user_verification_connection(connection_id) @@ -1730,6 +1737,33 @@ impl MessageProcessor { ClientRequest::BedrockSetup { params, .. } => { self.account_processor.bedrock_setup(params).await } + ClientRequest::GatewayOAuthRead { .. } => { + Box::pin(self.account_processor.gateway_oauth_read()) + .await + .map(|response| Some(response.into())) + } + ClientRequest::GatewayOAuthLogin { .. } => { + if session + .opted_out_notification_methods() + .contains("account/gatewayOAuth/changed") + { + Err(invalid_request( + "Gateway login requires account/gatewayOAuth/changed notifications", + )) + } else { + Box::pin( + self.account_processor + .gateway_oauth_login(connection_id, &session.rpc_gate), + ) + .await + .map(|response| Some(response.into())) + } + } + ClientRequest::GatewayOAuthCancel { .. } => { + Box::pin(self.account_processor.gateway_oauth_cancel(connection_id)) + .await + .map(|response| Some(response.into())) + } ClientRequest::LogoutAccount { .. } => { self.account_processor .logout_account(request_id.clone()) @@ -1849,3 +1883,7 @@ impl MessageProcessor { #[cfg(test)] #[path = "message_processor_tracing_tests.rs"] mod message_processor_tracing_tests; + +#[cfg(test)] +#[path = "message_processor_gateway_oauth_tests.rs"] +mod gateway_oauth_tests; diff --git a/codex-rs/app-server/src/message_processor_gateway_oauth_tests.rs b/codex-rs/app-server/src/message_processor_gateway_oauth_tests.rs new file mode 100644 index 0000000000..dd63e67183 --- /dev/null +++ b/codex-rs/app-server/src/message_processor_gateway_oauth_tests.rs @@ -0,0 +1,278 @@ +//! Exercise gateway sign-in and managed-provider checks through JSON-RPC with a mock keyring. + +use super::ConnectionSessionState; +use super::MessageProcessor; +use super::message_processor_tracing_tests::TEST_CONNECTION_ID; +use super::message_processor_tracing_tests::build_test_processor; +use super::message_processor_tracing_tests::read_response; +use crate::outgoing_message::OutgoingEnvelope; +use crate::outgoing_message::OutgoingMessage; +use crate::transport::AppServerTransport; +use anyhow::Result; +use codex_app_server_protocol::GatewayOAuthLoginResponse; +use codex_app_server_protocol::GatewayOAuthReadResponse; +use codex_app_server_protocol::GatewayOAuthStatus; +use codex_app_server_protocol::InitializeResponse; +use codex_app_server_protocol::JSONRPCErrorError; +use codex_app_server_protocol::ServerNotification; +use codex_core::config::ConfigBuilder; +use codex_login::AuthManager; +use codex_login::CodexAuth; +use codex_model_provider::test_support::seed_gateway_auth; +use pretty_assertions::assert_eq; +use serde_json::json; +use std::sync::Arc; +use std::time::Duration; +use tempfile::TempDir; +use tokio::time::timeout; +use wiremock::Mock; +use wiremock::MockServer; +use wiremock::ResponseTemplate; +use wiremock::matchers::body_string_contains; +use wiremock::matchers::method; +use wiremock::matchers::path; + +async fn send_request( + processor: &Arc, + session: &Arc, + request: serde_json::Value, +) { + processor + .process_request( + TEST_CONNECTION_ID, + serde_json::from_value(request).expect("JSON-RPC request"), + &AppServerTransport::Stdio, + Arc::clone(session), + ) + .await; +} + +#[tokio::test] +async fn gateway_oauth_login_updates_existing_requests() -> Result<()> { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/token")) + .and(body_string_contains("grant_type=authorization_code")) + .and(body_string_contains("code=accepted")) + .respond_with(ResponseTemplate::new(/*s*/ 200).set_body_json(json!({ + "access_token": "new-access", "refresh_token": "new-refresh", "expires_in": 3600 + }))) + .expect(/*r*/ 1) + .mount(&server) + .await; + let home = TempDir::new()?; + std::fs::write( + home.path().join("config.toml"), + format!( + r#" +model_provider = "gateway" +[model_providers.gateway] +name = "OpenAI" +requires_openai_auth = true +base_url = "{url}/v1" +wire_api = "responses" +[model_providers.gateway.gateway_oauth] +authorization_url = "{url}/authorize" +token_url = "{url}/token" +client_id = "app-server-login-success" +delivery = {{ kind = "header", name = "X-Gateway-Authorization" }} +"#, + url = server.uri() + ), + )?; + let config = Arc::new( + ConfigBuilder::default() + .codex_home(home.path().to_path_buf()) + .build() + .await?, + ); + let auth_manager = AuthManager::from_auth_for_testing_with_home( + CodexAuth::from_api_key("primary"), + home.path().to_path_buf(), + ); + let observer = seed_gateway_auth( + &config.model_provider, + &auth_manager, + json!({ + "access_token": "old-access", "refresh_token": "old-refresh", "expires_at": null, + }), + ); + assert_eq!(observer.resolve_access_token().await?, "old-access"); + let provider = codex_model_provider::create_model_provider( + config.model_provider.clone(), + Some(Arc::clone(&auth_manager)), + ); + assert_eq!( + provider.api_auth().await?.to_auth_headers()["x-gateway-authorization"], + "Bearer old-access" + ); + let (processor, mut outgoing) = build_test_processor(config, auth_manager).await; + let session = Arc::new(ConnectionSessionState::new( + crate::transport::ConnectionOrigin::Stdio, + )); + send_request( + &processor, + &session, + json!({ + "id": 1, "method": "initialize", "params": { + "clientInfo": {"name": "gateway-test", "version": "1"} + } + }), + ) + .await; + let _: InitializeResponse = read_response(&mut outgoing, /*request_id*/ 1).await; + send_request( + &processor, + &session, + json!({"id": 2, "method": "account/gatewayOAuth/login"}), + ) + .await; + let authorization_url = timeout(Duration::from_secs(/*secs*/ 10), async { + loop { + let envelope = outgoing + .recv() + .await + .expect("outgoing channel open during login"); + if let OutgoingEnvelope::ToConnection { + connection_id, + message: OutgoingMessage::AppServerNotification(notification), + .. + } = envelope + && let ServerNotification::GatewayOAuthChanged(changed) = notification.notification + && let Some(url) = changed.auth_url + { + assert_eq!(connection_id, TEST_CONNECTION_ID); + return url; + } + } + }) + .await?; + let query = url::Url::parse(&authorization_url)? + .query_pairs() + .into_owned() + .collect::>(); + let mut callback = url::Url::parse(&query["redirect_uri"])?; + callback + .query_pairs_mut() + .append_pair("code", "accepted") + .append_pair("state", &query["state"]); + codex_login::default_client::create_client_without_request_logging() + .get(callback.as_str()) + .send() + .await? + .error_for_status()?; + let response: GatewayOAuthLoginResponse = read_response(&mut outgoing, /*request_id*/ 2).await; + assert_eq!(response, GatewayOAuthLoginResponse {}); + send_request( + &processor, + &session, + json!({"id": 3, "method": "account/gatewayOAuth/read"}), + ) + .await; + let response: GatewayOAuthReadResponse = read_response(&mut outgoing, /*request_id*/ 3).await; + assert_eq!( + response, + GatewayOAuthReadResponse { + provider_id: "gateway".to_string(), + provider_name: "OpenAI".to_string(), + required: true, + status: Some(GatewayOAuthStatus::Succeeded), + error: None, + } + ); + assert_eq!(observer.resolve_access_token().await?, "new-access"); + assert_eq!( + provider.api_auth().await?.to_auth_headers()["x-gateway-authorization"], + "Bearer new-access" + ); + processor.shutdown_threads().await; + processor.drain_background_tasks().await; + Ok(()) +} + +#[tokio::test] +async fn gateway_oauth_model_list_checks_requirements_before_refreshing_token() -> Result<()> { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/token")) + .respond_with(ResponseTemplate::new(/*s*/ 200).set_body_json(json!({ + "access_token": "refreshed", "expires_in": 3600, + }))) + .mount(&server) + .await; + let home = TempDir::new()?; + let mut config = ConfigBuilder::default() + .codex_home(home.path().to_path_buf()) + .build() + .await?; + config.model_provider_id = "gateway".into(); + config.model_provider = toml::from_str(&format!( + r#" +name = "Gateway" +base_url = "{url}/v1" +[gateway_oauth] +authorization_url = "{url}/authorize" +token_url = "{url}/token" +client_id = "app-server-requirements-test" +delivery = {{ kind = "header", name = "X-Gateway-Authorization" }} +"#, + url = server.uri(), + ))?; + let auth_manager = AuthManager::from_auth_for_testing_with_home( + CodexAuth::from_api_key("primary"), + home.path().to_path_buf(), + ); + let _gateway = seed_gateway_auth( + &config.model_provider, + &auth_manager, + json!({"access_token": "expired", "refresh_token": "old-refresh", "expires_at": 0}), + ); + let (processor, mut outgoing) = build_test_processor(Arc::new(config), auth_manager).await; + let session = Arc::new(ConnectionSessionState::new( + crate::transport::ConnectionOrigin::Stdio, + )); + send_request( + &processor, + &session, + json!({"id": 1, "method": "initialize", "params": { + "clientInfo": {"name": "gateway-test", "version": "1"}, + "capabilities": {"explicitGatewayOauth": true}, + }}), + ) + .await; + let _: InitializeResponse = read_response(&mut outgoing, /*request_id*/ 1).await; + std::fs::write( + home.path().join("requirements.toml"), + "model_provider = 'openai'", + )?; + send_request( + &processor, + &session, + json!({"id": 2, "method": "model/list", "params": {}}), + ) + .await; + let error = timeout(Duration::from_secs(/*secs*/ 10), async { + loop { + if let OutgoingEnvelope::ToConnection { + message: OutgoingMessage::Error(error), + .. + } = outgoing + .recv() + .await + .expect("outgoing channel open during request") + { + break error.error; + } + } + }) + .await?; + assert_eq!(error, JSONRPCErrorError { + code: -32600, + message: "failed to load configuration: Your organization's required model provider settings changed. Restart Codex to apply them; this request was not sent".to_string(), + data: None, + }); + assert!(server.received_requests().await.unwrap().is_empty()); + processor.shutdown_threads().await; + processor.drain_background_tasks().await; + Ok(()) +} diff --git a/codex-rs/app-server/src/message_processor_tracing_tests.rs b/codex-rs/app-server/src/message_processor_tracing_tests.rs index 6715805dc9..41d843c743 100644 --- a/codex-rs/app-server/src/message_processor_tracing_tests.rs +++ b/codex-rs/app-server/src/message_processor_tracing_tests.rs @@ -49,12 +49,13 @@ use std::future::Future; use std::path::Path; use std::sync::Arc; use std::sync::OnceLock; +use std::time::Duration; use tempfile::TempDir; use tokio::sync::mpsc; use tracing_subscriber::layer::SubscriberExt; use wiremock::MockServer; -const TEST_CONNECTION_ID: ConnectionId = ConnectionId(7); +pub(super) const TEST_CONNECTION_ID: ConnectionId = ConnectionId(7); struct TestTracing { exporter: InMemorySpanExporter, @@ -120,7 +121,12 @@ impl TracingHarness { let server = create_mock_responses_server_repeating_assistant("Done").await; let codex_home = TempDir::new()?; let config = Arc::new(build_test_config(codex_home.path(), &server.uri()).await?); - let (processor, outgoing_rx) = build_test_processor(config).await; + let auth_manager = AuthManager::shared_from_config( + config.as_ref(), + /*enable_codex_api_key_env*/ false, + ) + .await?; + let (processor, outgoing_rx) = build_test_processor(config, auth_manager).await; let tracing = init_test_tracing(); tracing.exporter.reset(); tracing::callsite::rebuild_interest_cache(); @@ -229,21 +235,20 @@ async fn build_test_config(codex_home: &Path, server_uri: &str) -> Result, + auth_manager: Arc, ) -> ( Arc, mpsc::Receiver, ) { let (outgoing_tx, outgoing_rx) = mpsc::channel(16); - let auth_manager = - AuthManager::shared_from_config(config.as_ref(), /*enable_codex_api_key_env*/ false) - .await - .expect("test auth manager"); let config_manager = ConfigManager::new( config.codex_home.to_path_buf(), Vec::new(), - LoaderOverrides::default(), + LoaderOverrides::with_managed_config_path_for_tests( + config.codex_home.join("managed_config.toml").to_path_buf(), + ), /*strict_config*/ false, CloudConfigBundleLoader::default(), Arg0DispatchPaths::default(), @@ -432,12 +437,20 @@ fn assert_has_internal_descendant_at_min_depth( ); } -async fn read_response( +pub(super) async fn read_response( outgoing_rx: &mut mpsc::Receiver, request_id: i64, +) -> T { + read_response_from(outgoing_rx, TEST_CONNECTION_ID, request_id).await +} + +pub(super) async fn read_response_from( + outgoing_rx: &mut mpsc::Receiver, + expected_connection_id: ConnectionId, + request_id: i64, ) -> T { loop { - let envelope = tokio::time::timeout(std::time::Duration::from_secs(5), outgoing_rx.recv()) + let envelope = tokio::time::timeout(Duration::from_secs(/*secs*/ 30), outgoing_rx.recv()) .await .expect("timed out waiting for response") .expect("outgoing channel closed"); @@ -449,7 +462,7 @@ async fn read_response( else { continue; }; - if connection_id != TEST_CONNECTION_ID { + if connection_id != expected_connection_id { continue; } let crate::outgoing_message::OutgoingMessage::Response(response) = message else { diff --git a/codex-rs/app-server/src/notification_media.rs b/codex-rs/app-server/src/notification_media.rs index 04e57f8c53..a3bb780b36 100644 --- a/codex-rs/app-server/src/notification_media.rs +++ b/codex-rs/app-server/src/notification_media.rs @@ -134,7 +134,8 @@ pub(crate) fn without_notification_media(notification: ServerNotification) -> Se | ServerNotification::ThreadRealtimeClosed(_) | ServerNotification::WindowsWorldWritableWarning(_) | ServerNotification::WindowsSandboxSetupCompleted(_) - | ServerNotification::AccountLoginCompleted(_) => notification, + | ServerNotification::AccountLoginCompleted(_) + | ServerNotification::GatewayOAuthChanged(_) => notification, } } diff --git a/codex-rs/app-server/src/request_processors/account_processor.rs b/codex-rs/app-server/src/request_processors/account_processor.rs index 30d1ff7ab0..f30658f3d5 100644 --- a/codex-rs/app-server/src/request_processors/account_processor.rs +++ b/codex-rs/app-server/src/request_processors/account_processor.rs @@ -14,6 +14,7 @@ use codex_login::login_with_bedrock_access_keys; use codex_model_provider::is_supported_amazon_bedrock_region; mod bedrock_setup; +mod gateway_oauth; mod rate_limit_resets; mod workspace_routing; @@ -96,6 +97,9 @@ pub(crate) struct AccountRequestProcessor { workspace_routing: Arc>>, workspace_routing_fetches: Arc>, workspace_routing_shutdown: CancellationToken, + gateway_login: Arc>>, + gateway_client: Arc>>>, + _gateway_notifications: Arc>, } impl AccountRequestProcessor { @@ -106,13 +110,21 @@ impl AccountRequestProcessor { config: Arc, config_manager: ConfigManager, ) -> Arc { + let gateway_notifications = crate::gateway_oauth_notifications::spawn( + Arc::clone(&auth_manager), + config_manager.clone(), + Arc::clone(&outgoing), + ); let processor = Arc::new(Self { + _gateway_notifications: Arc::new(gateway_notifications), auth_manager, thread_manager, outgoing, config, config_manager, active_login: Arc::new(Mutex::new(None)), + gateway_login: Arc::new(std::sync::Mutex::new(/*t*/ None)), + gateway_client: Arc::new(std::sync::Mutex::new(/*t*/ None)), workspace_routing: Arc::new(Mutex::new(None)), workspace_routing_fetches: Arc::new(Mutex::new(HashMap::new())), workspace_routing_shutdown: CancellationToken::new(), @@ -197,6 +209,7 @@ impl AccountRequestProcessor { } pub(crate) async fn cancel_active_login(&self) { + self.cancel_gateway_login(); let mut guard = self.active_login.lock().await; if let Some(active_login) = guard.take() { drop(active_login); @@ -954,13 +967,7 @@ impl AccountRequestProcessor { } let config = self.load_latest_config().await; - // Cancel any active login attempt. - { - let mut guard = self.active_login.lock().await; - if let Some(active) = guard.take() { - drop(active); - } - } + self.cancel_active_login().await; match self.auth_manager.logout_with_revoke().await { Ok(_) => {} diff --git a/codex-rs/app-server/src/request_processors/account_processor/gateway_oauth.rs b/codex-rs/app-server/src/request_processors/account_processor/gateway_oauth.rs new file mode 100644 index 0000000000..c648eeea94 --- /dev/null +++ b/codex-rs/app-server/src/request_processors/account_processor/gateway_oauth.rs @@ -0,0 +1,180 @@ +//! Gateway sign-in RPCs share credentials with inference and bind cancellation to the initiating connection. +//! Cancellation is acknowledged only after the active login releases its slot. + +use super::*; +use crate::gateway_oauth_notifications::status_fields; +use crate::transport::ConnectionId; +use codex_app_server_protocol::GatewayOAuthCancelResponse; +use codex_app_server_protocol::GatewayOAuthLoginResponse; +use codex_app_server_protocol::GatewayOAuthReadResponse; +use codex_login::GatewayAuthManager; +use std::sync::PoisonError; + +pub(super) struct ActiveGatewayLogin { + owner: ConnectionId, + cancel: CancellationToken, + finished: CancellationToken, +} + +struct LoginGuard(Arc>>); + +impl Drop for LoginGuard { + fn drop(&mut self) { + if let Some(active) = self.0.lock().unwrap_or_else(PoisonError::into_inner).take() { + active.finished.cancel(); + } + } +} + +impl AccountRequestProcessor { + async fn gateway_client( + &self, + ) -> Result<(Config, Option>), JSONRPCErrorError> { + let config = self + .config_manager + .load_latest_config(/*fallback_cwd*/ None) + .await + .map_err(|err| { + internal_error(format!("failed to load gateway configuration: {err}")) + })?; + let manager = codex_model_provider::create_model_provider( + config.model_provider.clone(), + Some(Arc::clone(&self.auth_manager)), + ) + .gateway_auth_manager() + .map_err(|error| internal_error(error.to_string()))?; + *self + .gateway_client + .lock() + .unwrap_or_else(PoisonError::into_inner) = manager.clone(); + Ok((config, manager)) + } + + pub(crate) async fn gateway_oauth_read( + &self, + ) -> Result { + let (config, client) = self.gateway_client().await?; + let state = if let Some(client) = client { + Some(status_fields( + client + .status() + .await + .map_err(|err| internal_error(err.to_string()))?, + )) + } else { + None + }; + Ok(GatewayOAuthReadResponse { + provider_id: config.model_provider_id, + provider_name: config.model_provider.name, + required: state.is_some(), + status: state.as_ref().map(|state| state.0), + error: state.and_then(|state| state.1), + }) + } + + pub(crate) async fn gateway_oauth_login( + &self, + owner: ConnectionId, + connection_gate: &crate::connection_rpc_gate::ConnectionRpcGate, + ) -> Result { + let cancel = CancellationToken::new(); + let guard = { + let mut active = self + .gateway_login + .lock() + .unwrap_or_else(PoisonError::into_inner); + if connection_gate.is_closed() { + return Err(invalid_request("The connection is closed")); + } + if active.is_some() { + return Err(invalid_request("Gateway sign-in is already in progress")); + } + *active = Some(ActiveGatewayLogin { + owner, + cancel: cancel.clone(), + finished: CancellationToken::new(), + }); + LoginGuard(Arc::clone(&self.gateway_login)) + }; + let (config, client) = self.gateway_client().await?; + let client = client + .ok_or_else(|| invalid_request("The current provider does not use gateway OAuth"))?; + if config.model_provider.requires_openai_auth + && config.model_provider.env_key.is_none() + && config.model_provider.experimental_bearer_token.is_none() + && config.model_provider.auth.is_none() + && self.auth_manager.auth_cached().is_none() + { + return Err(invalid_request( + "Sign in to your primary account before signing in to the gateway", + )); + } + let (url_tx, url_rx) = tokio::sync::oneshot::channel(); + let login = client.login_with_browser(cancel.cancelled(), |url| { + let _ = url_tx.send(url.to_string()); + }); + tokio::pin!(login); + let result = tokio::select! { + result = &mut login => result, + url = url_rx => { + if let Ok(url) = url { + self.outgoing.send_server_notification_to_connections(&[owner], ServerNotification::GatewayOAuthChanged(codex_app_server_protocol::GatewayOAuthChangedNotification { + auth_url: Some(url), provider_id: config.model_provider_id.clone(), + status: codex_app_server_protocol::GatewayOAuthStatus::Started, error: None, + })).await; + } + login.await + } + }; + result.map_err(|err| internal_error(err.to_string()))?; + drop(guard); + Ok(GatewayOAuthLoginResponse {}) + } + + pub(crate) async fn gateway_oauth_cancel( + &self, + owner: ConnectionId, + ) -> Result { + let finished = { + let active = self + .gateway_login + .lock() + .unwrap_or_else(PoisonError::into_inner); + let Some(active) = active.as_ref() else { + return Ok(GatewayOAuthCancelResponse {}); + }; + if active.owner != owner { + return Err(invalid_request( + "Gateway sign-in belongs to another connection", + )); + } + active.cancel.cancel(); + active.finished.clone() + }; + finished.cancelled().await; + Ok(GatewayOAuthCancelResponse {}) + } + + pub(crate) fn gateway_connection_closed(&self, owner: ConnectionId) { + let active = self + .gateway_login + .lock() + .unwrap_or_else(PoisonError::into_inner); + if let Some(active) = active.as_ref() + && active.owner == owner + { + active.cancel.cancel(); + } + } + + pub(super) fn cancel_gateway_login(&self) { + let active = self + .gateway_login + .lock() + .unwrap_or_else(PoisonError::into_inner); + if let Some(active) = active.as_ref() { + active.cancel.cancel(); + } + } +} diff --git a/codex-rs/app-server/src/request_processors/catalog_processor.rs b/codex-rs/app-server/src/request_processors/catalog_processor.rs index cc11ef8737..01fce38b3f 100644 --- a/codex-rs/app-server/src/request_processors/catalog_processor.rs +++ b/codex-rs/app-server/src/request_processors/catalog_processor.rs @@ -178,6 +178,39 @@ impl CatalogRequestProcessor { &self, params: ModelListParams, ) -> Result, JSONRPCErrorError> { + // Gate the same provider used by the catalog, including when its model cache is warm. + // Resolving credentials may refresh them, but explicit host policy prevents browser login. + if let Some(gateway) = codex_model_provider::create_model_provider( + self.config.model_provider.clone(), + Some(self.thread_manager.auth_manager()), + ) + .gateway_auth_manager() + .map_err(|error| internal_error(error.to_string()))? + { + // Refreshing credentials can contact the gateway before the catalog's own check. + self.config_manager + .check_thread_model_provider(&self.config) + .await + .map_err(|err| config_load_error(&err))?; + if gateway.resolve_access_token().await.is_err() { + let current = self + .config_manager + .load_latest_config(/*fallback_cwd*/ None) + .await + .map_err(|err| config_load_error(&err))?; + // Login RPCs use current config, while the catalog retains its startup provider. + if current.model_provider_id != self.config.model_provider_id + || current.model_provider != self.config.model_provider + { + return Err(invalid_request( + "Model provider settings changed. Restart Codex to apply them, then retry fetching the model list", + )); + } + return Err(invalid_request( + "Gateway sign-in required or unavailable. Complete gateway sign-in and retry fetching the model list", + )); + } + } self.list_models(params) .await .map(|response| Some(response.into())) diff --git a/codex-rs/app-server/src/request_processors/initialize_processor.rs b/codex-rs/app-server/src/request_processors/initialize_processor.rs index 493867c5a5..9ee058aef7 100644 --- a/codex-rs/app-server/src/request_processors/initialize_processor.rs +++ b/codex-rs/app-server/src/request_processors/initialize_processor.rs @@ -20,6 +20,8 @@ const NON_ORIGINATING_CLIENT_NAMES: &[&str] = &["codex_app_server_daemon", "code #[derive(Clone)] pub(crate) struct InitializeRequestProcessor { + gateway_login_control: Arc, + gateway_login_initialized: Arc>, outgoing: Arc, analytics_events_client: AnalyticsEventsClient, config: Arc, @@ -30,6 +32,7 @@ pub(crate) struct InitializeRequestProcessor { impl InitializeRequestProcessor { pub(crate) fn new( + gateway_login_control: Arc, outgoing: Arc, analytics_events_client: AnalyticsEventsClient, config: Arc, @@ -38,6 +41,8 @@ impl InitializeRequestProcessor { user_verification: Arc, ) -> Self { Self { + gateway_login_control, + gateway_login_initialized: Arc::new(std::sync::OnceLock::new()), outgoing, analytics_events_client, config, @@ -144,6 +149,19 @@ impl InitializeRequestProcessor { .await; } + if capabilities.explicit_gateway_oauth || mutates_global_identity { + // Only the first originating client may restore legacy automatic login. + // Any explicit opt-in is sticky across subsequent connections. + self.gateway_login_initialized.get_or_init(|| { + if !capabilities.explicit_gateway_oauth { + self.gateway_login_control.allow_automatic_login(); + } + }); + if capabilities.explicit_gateway_oauth { + self.gateway_login_control.require_explicit_login(); + } + } + if mutates_global_identity { // Only real client initialization may mutate process-global client metadata. if let Err(error) = set_default_originator(originator.clone()) { diff --git a/codex-rs/app-server/tests/suite/v2/attestation.rs b/codex-rs/app-server/tests/suite/v2/attestation.rs index 8b6b30b519..1f982b3e5e 100644 --- a/codex-rs/app-server/tests/suite/v2/attestation.rs +++ b/codex-rs/app-server/tests/suite/v2/attestation.rs @@ -81,6 +81,7 @@ async fn attestation_generate_round_trip_adds_header_to_responses_websocket_hand version: "0.1.0".to_string(), }, Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: true, request_attestation: true, opt_out_notification_methods: None, diff --git a/codex-rs/app-server/tests/suite/v2/experimental_api.rs b/codex-rs/app-server/tests/suite/v2/experimental_api.rs index 70abf5a372..b849e6d665 100644 --- a/codex-rs/app-server/tests/suite/v2/experimental_api.rs +++ b/codex-rs/app-server/tests/suite/v2/experimental_api.rs @@ -40,6 +40,7 @@ async fn mock_experimental_method_requires_experimental_api_capability() -> Resu .initialize_with_capabilities( default_client_info(), Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: false, request_attestation: false, opt_out_notification_methods: None, @@ -77,6 +78,7 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R .initialize_with_capabilities( default_client_info(), Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: false, request_attestation: false, opt_out_notification_methods: None, @@ -134,6 +136,7 @@ async fn thread_memory_mode_set_requires_experimental_api_capability() -> Result .initialize_with_capabilities( default_client_info(), Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: false, request_attestation: false, opt_out_notification_methods: None, @@ -176,6 +179,7 @@ async fn settings_update_requires_experimental_api_capability(method: &str) -> R .initialize_with_capabilities( default_client_info(), Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: false, request_attestation: false, opt_out_notification_methods: None, @@ -215,6 +219,7 @@ async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result< .initialize_with_capabilities( default_client_info(), Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: false, request_attestation: false, opt_out_notification_methods: None, @@ -275,6 +280,7 @@ async fn thread_start_mock_field_requires_experimental_api_capability() -> Resul .initialize_with_capabilities( default_client_info(), Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: false, request_attestation: false, opt_out_notification_methods: None, @@ -318,6 +324,7 @@ async fn thread_start_without_dynamic_tools_allows_without_experimental_api_capa .initialize_with_capabilities( default_client_info(), Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: false, request_attestation: false, opt_out_notification_methods: None, @@ -360,6 +367,7 @@ async fn thread_start_granular_approval_policy_requires_experimental_api_capabil .initialize_with_capabilities( default_client_info(), Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: false, request_attestation: false, opt_out_notification_methods: None, diff --git a/codex-rs/app-server/tests/suite/v2/gateway_oauth_tests.rs b/codex-rs/app-server/tests/suite/v2/gateway_oauth_tests.rs new file mode 100644 index 0000000000..d28243b7ad --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/gateway_oauth_tests.rs @@ -0,0 +1,532 @@ +//! Gateway OAuth RPCs expose login progress, cancellation, and initialization policy. + +use anyhow::Context; +use anyhow::Result; +use app_test_support::TestAppServer; +use codex_app_server_protocol::ClientInfo; +use codex_app_server_protocol::ClientRequest; +use codex_app_server_protocol::GatewayOAuthCancelResponse; +use codex_app_server_protocol::GatewayOAuthChangedNotification; +use codex_app_server_protocol::GatewayOAuthReadResponse; +use codex_app_server_protocol::GatewayOAuthStatus; +use codex_app_server_protocol::InitializeCapabilities; +use codex_app_server_protocol::JSONRPCErrorError; +use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::ThreadStartParams; +use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::TurnCompletedNotification; +use codex_app_server_protocol::TurnStartParams; +use codex_app_server_protocol::TurnStartResponse; +use codex_app_server_protocol::TurnStatus; +use codex_app_server_protocol::UserInput; +use pretty_assertions::assert_eq; +use std::time::Duration; +use tempfile::TempDir; +use tokio::time::timeout; +use wiremock::Mock; +use wiremock::MockServer; +use wiremock::ResponseTemplate; +use wiremock::matchers::body_string_contains; +use wiremock::matchers::method; +use wiremock::matchers::path; + +fn gateway_config(oauth: &MockServer) -> String { + format!( + r#" +model_provider = "gateway" +[features] +secret_auth_storage = true +[model_providers.gateway] +name = "Test gateway" +base_url = "{url}/v1" +wire_api = "responses" +[model_providers.gateway.gateway_oauth] +authorization_url = "{url}/authorize" +token_url = "{url}/token" +client_id = "app-server-explicit-test" +delivery = {{ kind = "header", name = "X-Gateway-Authorization" }} +"#, + url = oauth.uri() + ) +} + +async fn gateway_server(home: &std::path::Path) -> Result { + let mut server = TestAppServer::builder() + .with_codex_home(home) + .build() + .await?; + server + .initialize_with_capabilities( + ClientInfo { + name: "gateway-test".to_string(), + title: None, + version: "1".to_string(), + }, + Some(InitializeCapabilities { + explicit_gateway_oauth: true, + experimental_api: true, + ..Default::default() + }), + ) + .await?; + Ok(server) +} + +async fn read_gateway(server: &mut TestAppServer) -> Result { + server + .request(|id| ClientRequest::GatewayOAuthRead { + request_id: id, + params: None, + }) + .await +} + +async fn cancel_gateway(server: &mut TestAppServer) -> Result { + server + .request(|id| ClientRequest::GatewayOAuthCancel { + request_id: id, + params: None, + }) + .await +} + +async fn authorization_url(server: &mut TestAppServer) -> Result { + timeout(TIMEOUT, async { + loop { + let changed: GatewayOAuthChangedNotification = server + .read_notification("account/gatewayOAuth/changed") + .await?; + if let Some(url) = changed.auth_url { + return Ok(url::Url::parse(&url)?); + } + } + }) + .await? +} + +const TIMEOUT: Duration = Duration::from_secs(/*secs*/ 30); + +#[tokio::test] +async fn model_list_requests_restart_after_gateway_provider_changes() -> Result<()> { + let oauth = MockServer::start().await; + let original = gateway_config(&oauth); + for replacement in [ + String::new(), // Switch to the default provider without gateway OAuth. + original.replace("/authorize", "/replacement-authorize"), + ] { + let home = TempDir::new()?; + let config_path = home.path().join("config.toml"); + std::fs::write(&config_path, &original)?; + let mut server = gateway_server(home.path()).await?; + std::fs::write(&config_path, replacement)?; + let models = server + .send_raw_request("model/list", Some(serde_json::json!({}))) + .await?; + let error = timeout( + TIMEOUT, + server.read_stream_until_error_message(RequestId::Integer(models)), + ) + .await??; + assert_eq!(error.error, JSONRPCErrorError { + code: -32600, + message: "Model provider settings changed. Restart Codex to apply them, then retry fetching the model list".to_string(), + data: None, + }); + } + Ok(()) +} + +#[tokio::test] +async fn token_exchange_failure_completes_login_and_updates_readiness() -> Result<()> { + let home = TempDir::new()?; + let oauth = MockServer::start().await; + let secret = "issuer-query-credential"; + Mock::given(method("POST")) + .and(path("/token")) + .and(body_string_contains("grant_type=authorization_code")) + .and(body_string_contains("code=accepted")) + .respond_with( + ResponseTemplate::new(/*s*/ 400) + .insert_header("x-request-id", secret) + .set_body_json(serde_json::json!({ + "error": "invalid_grant", "error_description": secret, + })), + ) + .expect(/*r*/ 1) + .mount(&oauth) + .await; + std::fs::write( + home.path().join("config.toml"), + gateway_config(&oauth).replace( + "/authorize\"", + &format!("/authorize?issuer_secret={secret}\""), + ), + )?; + let mut server = gateway_server(home.path()).await?; + let login = server + .send_raw_request("account/gatewayOAuth/login", /*params*/ None) + .await?; + let authorization_url = authorization_url(&mut server).await?; + let query = authorization_url + .query_pairs() + .into_owned() + .collect::>(); + let mut callback = url::Url::parse(query.get("redirect_uri").context("callback URI")?)?; + callback + .query_pairs_mut() + .append_pair("code", "accepted") + .append_pair("state", query.get("state").context("OAuth state")?); + codex_login::default_client::create_client_without_request_logging() + .get(callback.as_str()) + .send() + .await? + .error_for_status()?; + let error = timeout( + TIMEOUT, + server.read_stream_until_error_message(RequestId::Integer(login)), + ) + .await??; + let message = "Gateway sign-in failed; check the gateway configuration and credential store."; + assert_eq!(error.error.message, message); + let changed = timeout( + TIMEOUT, + server.read_stream_until_matching_notification("gateway login failure", |notification| { + notification.method == "account/gatewayOAuth/changed" + && notification + .params + .as_ref() + .is_some_and(|params| params["status"] == "failed") + }), + ) + .await??; + assert_eq!( + serde_json::from_value::( + changed.params.context("gateway status")? + )?, + GatewayOAuthChangedNotification { + provider_id: "gateway".into(), + status: GatewayOAuthStatus::Failed, + auth_url: None, + error: Some(message.into()), + } + ); + let state: GatewayOAuthReadResponse = read_gateway(&mut server).await?; + assert_eq!( + state, + GatewayOAuthReadResponse { + provider_id: "gateway".to_string(), + provider_name: "Test gateway".to_string(), + required: true, + status: Some(GatewayOAuthStatus::Failed), + error: Some(error.error.message), + } + ); + Ok(()) +} + +#[tokio::test] +async fn read_and_cancel_without_gateway_oauth() -> Result<()> { + let home = TempDir::new()?; + let mut server = gateway_server(home.path()).await?; + let state: GatewayOAuthReadResponse = read_gateway(&mut server).await?; + assert_eq!( + (state.required, state.status, state.error), + (false, None, None) + ); + let response: GatewayOAuthCancelResponse = cancel_gateway(&mut server).await?; + assert_eq!(response, GatewayOAuthCancelResponse {}); + let login = server + .send_raw_request("account/gatewayOAuth/login", /*params*/ None) + .await?; + let error = timeout( + TIMEOUT, + server.read_stream_until_error_message(RequestId::Integer(login)), + ) + .await??; + assert_eq!( + error.error.message, + "The current provider does not use gateway OAuth" + ); + Ok(()) +} + +#[tokio::test] +async fn primary_account_is_required_before_gateway_login() -> Result<()> { + let home = TempDir::new()?; + let oauth = MockServer::start().await; + let config = gateway_config(&oauth).replace( + "wire_api = \"responses\"", + "wire_api = \"responses\"\nrequires_openai_auth = true", + ); + std::fs::write(home.path().join("config.toml"), config)?; + let mut server = gateway_server(home.path()).await?; + let login = server + .send_raw_request("account/gatewayOAuth/login", /*params*/ None) + .await?; + let error = timeout( + TIMEOUT, + server.read_stream_until_error_message(RequestId::Integer(login)), + ) + .await??; + assert_eq!( + error.error.message, + "Sign in to your primary account before signing in to the gateway" + ); + assert!(oauth.received_requests().await.unwrap().is_empty()); + Ok(()) +} + +#[tokio::test] +async fn read_is_passive_and_canceled_login_can_be_retried_immediately() -> Result<()> { + let home = TempDir::new()?; + let oauth = MockServer::start().await; + let config = gateway_config(&oauth); + std::fs::write(home.path().join("config.toml"), &config)?; + let mut server = gateway_server(home.path()).await?; + let state: GatewayOAuthReadResponse = read_gateway(&mut server).await?; + assert_eq!( + state, + GatewayOAuthReadResponse { + provider_id: "gateway".to_string(), + provider_name: "Test gateway".to_string(), + required: true, + status: Some(GatewayOAuthStatus::NotReady), + error: None + } + ); + assert!(oauth.received_requests().await.unwrap().is_empty()); + let expected = GatewayOAuthChangedNotification { + provider_id: "gateway".to_string(), + status: GatewayOAuthStatus::NotReady, + auth_url: None, + error: None, + }; + // Consume the initial readiness change before checking repeated request failures. + let changed: GatewayOAuthChangedNotification = timeout( + TIMEOUT, + server.read_notification("account/gatewayOAuth/changed"), + ) + .await??; + assert_eq!(changed, expected); + for _ in 0..2 { + let models = server + .send_raw_request("model/list", Some(serde_json::json!({}))) + .await?; + let error = timeout( + TIMEOUT, + server.read_stream_until_error_message(RequestId::Integer(models)), + ) + .await??; + assert!(error.error.message.contains("Gateway sign-in required")); + let changed: GatewayOAuthChangedNotification = timeout( + TIMEOUT, + server.read_notification("account/gatewayOAuth/changed"), + ) + .await??; + assert_eq!(changed, expected); + } + let login = server + .send_raw_request("account/gatewayOAuth/login", /*params*/ None) + .await?; + let url = authorization_url(&mut server).await?; + assert!( + url.as_str() + .starts_with(&format!("{}/authorize?", oauth.uri())) + ); + let state: GatewayOAuthReadResponse = read_gateway(&mut server).await?; + assert_eq!(state.status, Some(GatewayOAuthStatus::Started)); + let duplicate = server + .send_raw_request("account/gatewayOAuth/login", /*params*/ None) + .await?; + let error = timeout( + TIMEOUT, + server.read_stream_until_error_message(RequestId::Integer(duplicate)), + ) + .await??; + assert_eq!( + error.error.message, + "Gateway sign-in is already in progress" + ); + let _: GatewayOAuthCancelResponse = cancel_gateway(&mut server).await?; + // Retry on the cancel acknowledgment, without waiting for the old login response. + let retry = server + .send_raw_request("account/gatewayOAuth/login", /*params*/ None) + .await?; + authorization_url(&mut server).await?; + let _: GatewayOAuthCancelResponse = cancel_gateway(&mut server).await?; + for request_id in [login, retry] { + let error = timeout( + TIMEOUT, + server.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + assert_eq!(error.error.message, "Gateway sign-in was canceled"); + } + assert!(oauth.received_requests().await.unwrap().is_empty()); + + let state: GatewayOAuthReadResponse = read_gateway(&mut server).await?; + assert_eq!( + (state.status, state.error), + ( + Some(GatewayOAuthStatus::Failed), + Some("Gateway sign-in was canceled".to_string()) + ) + ); + + Ok(()) +} + +#[tokio::test] +async fn disconnect_cancels_only_the_owning_connections_login() -> Result<()> { + use super::connection_handling_websocket::connect_websocket; + use super::connection_handling_websocket::read_error_for_id; + use super::connection_handling_websocket::read_notification_for_method; + use super::connection_handling_websocket::read_response_for_id; + use super::connection_handling_websocket::send_request; + use super::connection_handling_websocket::spawn_websocket_server; + let home = TempDir::new()?; + let oauth = MockServer::start().await; + std::fs::write(home.path().join("config.toml"), gateway_config(&oauth))?; + let (mut process, address) = spawn_websocket_server(home.path()).await?; + let mut owner = connect_websocket(address).await?; + let mut observer = connect_websocket(address).await?; + send_request(&mut owner, "initialize", /*id*/ 1, Some(serde_json::json!({"clientInfo": {"name": "gateway-owner", "version": "1"}, "capabilities": {"experimentalApi": true, "explicitGatewayOauth": true}}))).await?; + read_response_for_id(&mut owner, /*id*/ 1).await?; + send_request(&mut observer, "initialize", /*id*/ 1, Some(serde_json::json!({"clientInfo": {"name": "gateway-observer", "version": "1"}, "capabilities": {"experimentalApi": true}}))).await?; + read_response_for_id(&mut observer, /*id*/ 1).await?; + // A legacy connection cannot undo another connection's explicit opt-in. + send_request( + &mut observer, + "model/list", + /*id*/ 0, + Some(serde_json::json!({})), + ) + .await?; + let error = read_error_for_id(&mut observer, /*id*/ 0).await?; + assert!(error.error.message.contains("Gateway sign-in required")); + send_request( + &mut owner, + "account/gatewayOAuth/login", + /*id*/ 2, + /*params*/ None, + ) + .await?; + // Startup can publish readiness before the login request starts authorization. + timeout(TIMEOUT, async { + loop { + let changed = + read_notification_for_method(&mut observer, "account/gatewayOAuth/changed").await?; + if changed + .params + .as_ref() + .and_then(|params| params["status"].as_str()) + == Some("started") + { + return anyhow::Ok(()); + } + } + }) + .await??; + send_request( + &mut observer, + "account/gatewayOAuth/cancel", + /*id*/ 2, + /*params*/ None, + ) + .await?; + let error = read_error_for_id(&mut observer, /*id*/ 2).await?; + assert_eq!( + error.error.message, + "Gateway sign-in belongs to another connection" + ); + owner.close(/*msg*/ None).await?; + let canceled = + read_notification_for_method(&mut observer, "account/gatewayOAuth/changed").await?; + assert_eq!(canceled.params.unwrap()["status"], "failed"); + send_request( + &mut observer, + "account/gatewayOAuth/login", + /*id*/ 3, + /*params*/ None, + ) + .await?; + let started = + read_notification_for_method(&mut observer, "account/gatewayOAuth/changed").await?; + assert_eq!(started.params.unwrap()["status"], "started"); + send_request( + &mut observer, + "account/gatewayOAuth/cancel", + /*id*/ 4, + /*params*/ None, + ) + .await?; + // Stop the test process after observing that a new connection can own a new login. + process.kill().await?; + Ok(()) +} + +#[tokio::test] +async fn initialization_selects_legacy_or_explicit_gateway_login() -> Result<()> { + for explicit in [false, true] { + let home = TempDir::new()?; + let oauth = MockServer::start().await; + // Force automatic authorization to fail before launching a real browser. + let callback = std::net::TcpListener::bind("127.0.0.1:0")?; + let port = callback.local_addr()?.port(); + std::fs::write( + home.path().join("config.toml"), + format!("{}\nredirect_port = {port}\n", gateway_config(&oauth)), + )?; + let mut server = TestAppServer::builder() + .with_codex_home(home.path()) + .build() + .await?; + server + .initialize_with_capabilities( + ClientInfo { + name: "gateway-compat-test".into(), + title: None, + version: "1".into(), + }, + Some(InitializeCapabilities { + explicit_gateway_oauth: explicit, + experimental_api: true, + ..Default::default() + }), + ) + .await?; + std::fs::write( + home.path().join("config.toml"), + format!("{}\nredirect_port = {port}\n", gateway_config(&oauth)) + .replace("app-server-explicit-test", "gateway-after-initialize"), + )?; + let thread = server + .send_thread_start_request_with_auto_env(ThreadStartParams::default()) + .await?; + let thread: ThreadStartResponse = server.read_response(thread).await?; + let turn = server + .send_turn_start_request(TurnStartParams { + thread_id: thread.thread.id, + input: vec![UserInput::Text { + text: "hello".into(), + text_elements: vec![], + }], + ..Default::default() + }) + .await?; + let _: TurnStartResponse = server.read_response(turn).await?; + let completed: TurnCompletedNotification = + timeout(TIMEOUT, server.read_notification("turn/completed")).await??; + assert_eq!(completed.turn.status, TurnStatus::Failed); + assert_eq!( + completed.turn.error.context("gateway failure")?.message, + if explicit { + "Gateway sign-in required. Choose Sign in or Reconnect, then retry your request" + } else { + "Gateway OAuth authentication failed; check the gateway configuration and credential store." + } + ); + assert!(oauth.received_requests().await.unwrap().is_empty()); + } + Ok(()) +} diff --git a/codex-rs/app-server/tests/suite/v2/initialize.rs b/codex-rs/app-server/tests/suite/v2/initialize.rs index 923bb41535..1ba0f266c9 100644 --- a/codex-rs/app-server/tests/suite/v2/initialize.rs +++ b/codex-rs/app-server/tests/suite/v2/initialize.rs @@ -240,6 +240,7 @@ async fn initialize_opt_out_notification_methods_filters_notifications() -> Resu version: "0.1.0".to_string(), }, Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: true, request_attestation: false, opt_out_notification_methods: Some(vec!["thread/started".to_string()]), diff --git a/codex-rs/app-server/tests/suite/v2/mcp_tool.rs b/codex-rs/app-server/tests/suite/v2/mcp_tool.rs index a0c313b7e2..f346144a71 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_tool.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_tool.rs @@ -259,6 +259,7 @@ async fn mcp_server_tool_call_forwards_only_server_extensions() -> Result<()> { version: "0.1.0".to_string(), }, Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: true, request_attestation: false, mcp_server_openai_form_elicitation: true, @@ -354,6 +355,7 @@ async fn model_mcp_tool_call_uses_session_client_extensions() -> Result<()> { version: "0.1.0".to_string(), }, Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: true, request_attestation: false, mcp_server_openai_form_elicitation: true, diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index 55e735aa0b..09856f5306 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -52,6 +52,8 @@ mod external_agent_config; mod external_agent_import_sync; mod feedback; mod fs; +#[path = "gateway_oauth_tests.rs"] +mod gateway_oauth; mod git_attribution; mod guardian_v2; mod history_notes_extension; diff --git a/codex-rs/app-server/tests/suite/v2/thread_revert.rs b/codex-rs/app-server/tests/suite/v2/thread_revert.rs index a8a9cc8a49..4b74e297f1 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_revert.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_revert.rs @@ -655,6 +655,7 @@ async fn initialize_experimental(mcp: &mut TestAppServer) -> Result<()> { version: "0.1.0".to_string(), }, Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: true, request_attestation: false, opt_out_notification_methods: None, diff --git a/codex-rs/app-server/tests/suite/v2/thread_status.rs b/codex-rs/app-server/tests/suite/v2/thread_status.rs index 9c51958f97..23031f6d9e 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_status.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_status.rs @@ -148,6 +148,7 @@ async fn thread_status_changed_can_be_opted_out() -> Result<()> { version: "0.1.0".to_string(), }, Some(InitializeCapabilities { + explicit_gateway_oauth: false, experimental_api: true, request_attestation: false, opt_out_notification_methods: Some(vec!["thread/status/changed".to_string()]), diff --git a/codex-rs/tui/src/app/app_server_event_targets.rs b/codex-rs/tui/src/app/app_server_event_targets.rs index 4753a2afde..eb5ffa6ecf 100644 --- a/codex-rs/tui/src/app/app_server_event_targets.rs +++ b/codex-rs/tui/src/app/app_server_event_targets.rs @@ -194,6 +194,7 @@ pub(super) fn server_notification_thread_target( | ServerNotification::SkillsChanged(_) | ServerNotification::McpServerOauthLoginCompleted(_) | ServerNotification::AccountUpdated(_) + | ServerNotification::GatewayOAuthChanged(_) | ServerNotification::AccountRateLimitsUpdated(_) | ServerNotification::AppListUpdated(_) | ServerNotification::EnvironmentConnected(_) diff --git a/codex-rs/tui/src/chatwidget/protocol.rs b/codex-rs/tui/src/chatwidget/protocol.rs index c5a887a957..dba652336a 100644 --- a/codex-rs/tui/src/chatwidget/protocol.rs +++ b/codex-rs/tui/src/chatwidget/protocol.rs @@ -341,6 +341,7 @@ impl ChatWidget { } ServerNotification::ServerRequestResolved(_) | ServerNotification::AccountUpdated(_) + | ServerNotification::GatewayOAuthChanged(_) | ServerNotification::AccountRateLimitsUpdated(_) | ServerNotification::ThreadStarted(_) | ServerNotification::ThreadStatusChanged(_) diff --git a/sdk/python/src/openai_codex/generated/notification_registry.py b/sdk/python/src/openai_codex/generated/notification_registry.py index 2fbb5048f8..b30783706d 100644 --- a/sdk/python/src/openai_codex/generated/notification_registry.py +++ b/sdk/python/src/openai_codex/generated/notification_registry.py @@ -27,6 +27,7 @@ from .v2_all import FileChangePatchUpdatedNotification from .v2_all import FsChangedNotification from .v2_all import FuzzyFileSearchSessionCompletedNotification from .v2_all import FuzzyFileSearchSessionUpdatedNotification +from .v2_all import GatewayOAuthChangedNotification from .v2_all import GuardianWarningNotification from .v2_all import HookCompletedNotification from .v2_all import HookStartedNotification @@ -109,6 +110,7 @@ KnownNotificationPayload: TypeAlias = ( | FsChangedNotification | FuzzyFileSearchSessionCompletedNotification | FuzzyFileSearchSessionUpdatedNotification + | GatewayOAuthChangedNotification | GuardianWarningNotification | HookCompletedNotification | HookStartedNotification @@ -172,6 +174,7 @@ KnownNotificationPayload: TypeAlias = ( ) NOTIFICATION_MODELS: dict[str, type[KnownNotificationPayload]] = { + "account/gatewayOAuth/changed": GatewayOAuthChangedNotification, "account/login/completed": AccountLoginCompletedNotification, "account/rateLimits/updated": AccountRateLimitsUpdatedNotification, "account/updated": AccountUpdatedNotification, diff --git a/sdk/python/src/openai_codex/generated/v2_all.py b/sdk/python/src/openai_codex/generated/v2_all.py index f1c6b5b05e..02bb318481 100644 --- a/sdk/python/src/openai_codex/generated/v2_all.py +++ b/sdk/python/src/openai_codex/generated/v2_all.py @@ -1830,6 +1830,27 @@ class FuzzyFileSearchSessionUpdatedNotification(BaseModel): session_id: Annotated[str, Field(alias="sessionId")] +class GatewayOAuthCancelResponse(BaseModel): + pass + model_config = ConfigDict( + populate_by_name=True, + ) + + +class GatewayOAuthLoginResponse(BaseModel): + pass + model_config = ConfigDict( + populate_by_name=True, + ) + + +class GatewayOAuthStatus(Enum): + not_ready = "notReady" + started = "started" + succeeded = "succeeded" + failed = "failed" + + class GetAccountParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -2094,6 +2115,13 @@ class InitializeCapabilities(BaseModel): description="Opt into receiving experimental API methods and fields.", ), ] = False + explicit_gateway_oauth: Annotated[ + bool | None, + Field( + alias="explicitGatewayOauth", + description="Use explicit gateway OAuth login instead of automatic browser authorization. Applies to this app-server's gateway runtime; later connections cannot undo it.", + ), + ] = None extensions: Annotated[ dict[str, Any] | None, Field(description="MCP extension settings declared by the app-server client."), @@ -7249,6 +7277,41 @@ class ModelListRequest(BaseModel): params: ModelListParams +class AccountGatewayOAuthReadRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["account/gatewayOAuth/read"], Field(title="Account/gatewayOAuth/readRequestMethod") + ] + params: None = None + + +class AccountGatewayOAuthLoginRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["account/gatewayOAuth/login"], + Field(title="Account/gatewayOAuth/loginRequestMethod"), + ] + params: None = None + + +class AccountGatewayOAuthCancelRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["account/gatewayOAuth/cancel"], + Field(title="Account/gatewayOAuth/cancelRequestMethod"), + ] + params: None = None + + class ModelProviderCapabilitiesReadRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -8092,6 +8155,41 @@ class FunctionCallOutputContentItem( ] +class GatewayOAuthChangedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + auth_url: Annotated[ + str | None, + Field( + alias="authUrl", + description="Authorization handoff, sent only to the connection that started login.", + ), + ] = None + error: str | None = None + provider_id: Annotated[str, Field(alias="providerId")] + status: GatewayOAuthStatus + + +class GatewayOAuthReadResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + error: str | None = None + provider_id: Annotated[str, Field(alias="providerId")] + provider_name: Annotated[str, Field(alias="providerName")] + required: Annotated[ + bool, + Field( + description="Whether the selected provider uses gateway OAuth, even when already signed in." + ), + ] + status: Annotated[ + GatewayOAuthStatus | None, + Field(description="Null when the effective provider does not use gateway OAuth."), + ] = None + + class GetAccountResponse(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -9207,6 +9305,24 @@ class AccountUpdatedServerNotification(BaseModel): params: AccountUpdatedNotification +class AccountGatewayOAuthChangedServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + emitted_at_ms: Annotated[ + int | None, + Field( + alias="emittedAtMs", + description="Unix timestamp (in milliseconds) when app-server emitted this notification.", + ), + ] = None + method: Annotated[ + Literal["account/gatewayOAuth/changed"], + Field(title="Account/gatewayOAuth/changedNotificationMethod"), + ] + params: GatewayOAuthChangedNotification + + class TurnModerationMetadataServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -12337,6 +12453,9 @@ class ClientRequest( | TurnInterruptRequest | ReviewStartRequest | ModelListRequest + | AccountGatewayOAuthReadRequest + | AccountGatewayOAuthLoginRequest + | AccountGatewayOAuthCancelRequest | ModelProviderCapabilitiesReadRequest | ExperimentalFeatureListRequest | PermissionProfileListRequest @@ -12444,6 +12563,9 @@ class ClientRequest( | TurnInterruptRequest | ReviewStartRequest | ModelListRequest + | AccountGatewayOAuthReadRequest + | AccountGatewayOAuthLoginRequest + | AccountGatewayOAuthCancelRequest | ModelProviderCapabilitiesReadRequest | ExperimentalFeatureListRequest | PermissionProfileListRequest @@ -12701,6 +12823,7 @@ class ServerNotification( | McpServerStartupStatusUpdatedServerNotification | McpServerEventStreamNotificationServerNotification | AccountUpdatedServerNotification + | AccountGatewayOAuthChangedServerNotification | AccountRateLimitsUpdatedServerNotification | AppListUpdatedServerNotification | RemoteControlStatusChangedServerNotification @@ -12789,6 +12912,7 @@ class ServerNotification( | McpServerStartupStatusUpdatedServerNotification | McpServerEventStreamNotificationServerNotification | AccountUpdatedServerNotification + | AccountGatewayOAuthChangedServerNotification | AccountRateLimitsUpdatedServerNotification | AppListUpdatedServerNotification | RemoteControlStatusChangedServerNotification