diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 8cf96296d8..1d7177e067 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1678,6 +1678,32 @@ ], "type": "object" }, + "GatewayOAuthLoginParams": { + "description": "Authorize a configured provider without changing the default or any thread's provider.", + "properties": { + "modelProvider": { + "description": "Defaults to the current provider when omitted.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "GatewayOAuthReadParams": { + "description": "Read credential readiness for a configured provider without starting browser login.", + "properties": { + "modelProvider": { + "description": "Defaults to the current provider when omitted.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "GcpProviderId": { "enum": [ "gcp" @@ -8185,7 +8211,14 @@ "type": "string" }, "params": { - "type": "null" + "anyOf": [ + { + "$ref": "#/definitions/GatewayOAuthReadParams" + }, + { + "type": "null" + } + ] } }, "required": [ @@ -8208,7 +8241,14 @@ "type": "string" }, "params": { - "type": "null" + "anyOf": [ + { + "$ref": "#/definitions/GatewayOAuthLoginParams" + }, + { + "type": "null" + } + ] } }, "required": [ 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 b7800e41c5..3db9081733 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 @@ -1850,7 +1850,14 @@ "type": "string" }, "params": { - "type": "null" + "anyOf": [ + { + "$ref": "#/definitions/v2/GatewayOAuthReadParams" + }, + { + "type": "null" + } + ] } }, "required": [ @@ -1873,7 +1880,14 @@ "type": "string" }, "params": { - "type": "null" + "anyOf": [ + { + "$ref": "#/definitions/v2/GatewayOAuthLoginParams" + }, + { + "type": "null" + } + ] } }, "required": [ @@ -13177,11 +13191,37 @@ "title": "GatewayOAuthChangedNotification", "type": "object" }, + "GatewayOAuthLoginParams": { + "description": "Authorize a configured provider without changing the default or any thread's provider.", + "properties": { + "modelProvider": { + "description": "Defaults to the current provider when omitted.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "GatewayOAuthLoginResponse": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "GatewayOAuthLoginResponse", "type": "object" }, + "GatewayOAuthReadParams": { + "description": "Read credential readiness for a configured provider without starting browser login.", + "properties": { + "modelProvider": { + "description": "Defaults to the current provider when omitted.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "GatewayOAuthReadResponse": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Current effective gateway policy and credential readiness; never contains credentials.", @@ -16886,6 +16926,30 @@ ], "type": "string" }, + "NullableGatewayOAuthLoginParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "$ref": "#/definitions/v2/GatewayOAuthLoginParams" + }, + { + "type": "null" + } + ], + "title": "Nullable_GatewayOAuthLoginParams" + }, + "NullableGatewayOAuthReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "$ref": "#/definitions/v2/GatewayOAuthReadParams" + }, + { + "type": "null" + } + ], + "title": "Nullable_GatewayOAuthReadParams" + }, "NullableGetAccountRateLimitsParams": { "$schema": "http://json-schema.org/draft-07/schema#", "anyOf": [ 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 8b39b75701..8eb9806c3b 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 @@ -3594,7 +3594,14 @@ "type": "string" }, "params": { - "type": "null" + "anyOf": [ + { + "$ref": "#/definitions/GatewayOAuthReadParams" + }, + { + "type": "null" + } + ] } }, "required": [ @@ -3617,7 +3624,14 @@ "type": "string" }, "params": { - "type": "null" + "anyOf": [ + { + "$ref": "#/definitions/GatewayOAuthLoginParams" + }, + { + "type": "null" + } + ] } }, "required": [ @@ -9069,11 +9083,37 @@ "title": "GatewayOAuthChangedNotification", "type": "object" }, + "GatewayOAuthLoginParams": { + "description": "Authorize a configured provider without changing the default or any thread's provider.", + "properties": { + "modelProvider": { + "description": "Defaults to the current provider when omitted.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "GatewayOAuthLoginResponse": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "GatewayOAuthLoginResponse", "type": "object" }, + "GatewayOAuthReadParams": { + "description": "Read credential readiness for a configured provider without starting browser login.", + "properties": { + "modelProvider": { + "description": "Defaults to the current provider when omitted.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "GatewayOAuthReadResponse": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Current effective gateway policy and credential readiness; never contains credentials.", @@ -12847,6 +12887,30 @@ ], "type": "string" }, + "NullableGatewayOAuthLoginParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "$ref": "#/definitions/GatewayOAuthLoginParams" + }, + { + "type": "null" + } + ], + "title": "Nullable_GatewayOAuthLoginParams" + }, + "NullableGatewayOAuthReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "$ref": "#/definitions/GatewayOAuthReadParams" + }, + { + "type": "null" + } + ], + "title": "Nullable_GatewayOAuthReadParams" + }, "NullableGetAccountRateLimitsParams": { "$schema": "http://json-schema.org/draft-07/schema#", "anyOf": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/NullableGatewayOAuthLoginParams.json b/codex-rs/app-server-protocol/schema/json/v2/NullableGatewayOAuthLoginParams.json new file mode 100644 index 0000000000..62155cbc02 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/NullableGatewayOAuthLoginParams.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "$ref": "#/definitions/GatewayOAuthLoginParams" + }, + { + "type": "null" + } + ], + "definitions": { + "GatewayOAuthLoginParams": { + "description": "Authorize a configured provider without changing the default or any thread's provider.", + "properties": { + "modelProvider": { + "description": "Defaults to the current provider when omitted.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + } + }, + "title": "Nullable_GatewayOAuthLoginParams" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/NullableGatewayOAuthReadParams.json b/codex-rs/app-server-protocol/schema/json/v2/NullableGatewayOAuthReadParams.json new file mode 100644 index 0000000000..96aae83359 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/NullableGatewayOAuthReadParams.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "$ref": "#/definitions/GatewayOAuthReadParams" + }, + { + "type": "null" + } + ], + "definitions": { + "GatewayOAuthReadParams": { + "description": "Read credential readiness for a configured provider without starting browser login.", + "properties": { + "modelProvider": { + "description": "Defaults to the current provider when omitted.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + } + }, + "title": "Nullable_GatewayOAuthReadParams" +} \ 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 97915668e6..a078efead8 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 a847376b72..5f037c463e 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 a2c362436a..be2e11da52 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -35,6 +35,8 @@ import type { FsRemoveParams } from "./v2/FsRemoveParams"; import type { FsUnwatchParams } from "./v2/FsUnwatchParams"; import type { FsWatchParams } from "./v2/FsWatchParams"; import type { FsWriteFileParams } from "./v2/FsWriteFileParams"; +import type { GatewayOAuthLoginParams } from "./v2/GatewayOAuthLoginParams"; +import type { GatewayOAuthReadParams } from "./v2/GatewayOAuthReadParams"; import type { GetAccountParams } from "./v2/GetAccountParams"; import type { GetAccountRateLimitsParams } from "./v2/GetAccountRateLimitsParams"; import type { GetAccountTokenUsageParams } from "./v2/GetAccountTokenUsageParams"; @@ -108,4 +110,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/attachmentOwner/list", id: RequestId, params: ThreadAttachmentOwnerListParams, } | { "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/list", id: RequestId, params: ModelProviderListParams, } | { "method": "modelProvider/config/write", id: RequestId, params: ModelProviderConfigWriteParams, } | { "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": "feedback/save", id: RequestId, params: FeedbackSaveParams, } | { "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/attachmentOwner/list", id: RequestId, params: ThreadAttachmentOwnerListParams, } | { "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?: GatewayOAuthReadParams | null | undefined, } | { "method": "account/gatewayOAuth/login", id: RequestId, params?: GatewayOAuthLoginParams | null | undefined, } | { "method": "account/gatewayOAuth/cancel", id: RequestId, params: undefined, } | { "method": "modelProvider/list", id: RequestId, params: ModelProviderListParams, } | { "method": "modelProvider/config/write", id: RequestId, params: ModelProviderConfigWriteParams, } | { "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": "feedback/save", id: RequestId, params: FeedbackSaveParams, } | { "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/v2/GatewayOAuthLoginParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthLoginParams.ts new file mode 100644 index 0000000000..5f447f017c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthLoginParams.ts @@ -0,0 +1,12 @@ +// 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. + +/** + * Authorize a configured provider without changing the default or any thread's provider. + */ +export type GatewayOAuthLoginParams = { +/** + * Defaults to the current provider when omitted. + */ +modelProvider?: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthReadParams.ts new file mode 100644 index 0000000000..6544347d03 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/GatewayOAuthReadParams.ts @@ -0,0 +1,12 @@ +// 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. + +/** + * Read credential readiness for a configured provider without starting browser login. + */ +export type GatewayOAuthReadParams = { +/** + * Defaults to the current provider when omitted. + */ +modelProvider?: string | null, }; 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 4667924b9e..ae7f81033c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -208,7 +208,9 @@ export type { FsWriteFileResponse } from "./FsWriteFileResponse"; export type { GatewayAuth } from "./GatewayAuth"; export type { GatewayOAuthCancelResponse } from "./GatewayOAuthCancelResponse"; export type { GatewayOAuthChangedNotification } from "./GatewayOAuthChangedNotification"; +export type { GatewayOAuthLoginParams } from "./GatewayOAuthLoginParams"; export type { GatewayOAuthLoginResponse } from "./GatewayOAuthLoginResponse"; +export type { GatewayOAuthReadParams } from "./GatewayOAuthReadParams"; export type { GatewayOAuthReadResponse } from "./GatewayOAuthReadResponse"; export type { GatewayOAuthStatus } from "./GatewayOAuthStatus"; export type { GcpProviderId } from "./GcpProviderId"; diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index 169362fa9f..7a98c22efb 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -2206,6 +2206,16 @@ mod tests { "params?: GetAccountRateLimitsParams | undefined, }" ); assert!(client_request_ts.contains(LEGACY_ACCOUNT_RATE_LIMITS_REQUEST)); + const LEGACY_GATEWAY_READ_REQUEST: &str = concat!( + "{ \"method\": \"account/gatewayOAuth/read\", id: RequestId, ", + "params?: GatewayOAuthReadParams | null | undefined, }" + ); + assert!(client_request_ts.contains(LEGACY_GATEWAY_READ_REQUEST)); + const LEGACY_GATEWAY_LOGIN_REQUEST: &str = concat!( + "{ \"method\": \"account/gatewayOAuth/login\", id: RequestId, ", + "params?: GatewayOAuthLoginParams | null | undefined, }" + ); + assert!(client_request_ts.contains(LEGACY_GATEWAY_LOGIN_REQUEST)); let account_usage_response_ts = std::str::from_utf8( fixture_tree .get(Path::new("v2/GetAccountTokenUsageResponse.ts")) @@ -2290,18 +2300,22 @@ mod tests { }); let contents = std::str::from_utf8(contents)?; - // Both stable usage RPCs originally required `params: undefined`. Preserve that + // These existing RPCs originally required `params: undefined`. Preserve that // source compatibility only for these exact envelopes, not arbitrary new fields. let checked_contents = if path == Path::new("ClientRequest.ts") { contents .replace(LEGACY_ACCOUNT_USAGE_REQUEST, "") .replace(LEGACY_ACCOUNT_RATE_LIMITS_REQUEST, "") + .replace(LEGACY_GATEWAY_READ_REQUEST, "") + .replace(LEGACY_GATEWAY_LOGIN_REQUEST, "") } else { contents.to_owned() }; if checked_contents.contains("| undefined") { undefined_offenders.push(path.clone()); } + // The exact gateway envelopes above also accept legacy null params. + let contents = checked_contents.as_str(); const SKIP_PREFIXES: &[&str] = &[ "const ", diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 7c24e86dba..8da1a8a4b8 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -499,6 +499,26 @@ enum GetAccountRateLimitsParamsTypeScript { Undefined, } +/// Preserve null and explicit undefined params while adding provider selection to gateway reads. +#[allow(dead_code)] +#[derive(TS)] +#[ts(untagged)] +enum GatewayOAuthReadParamsTypeScript { + Params(Option), + #[ts(type = "undefined")] + Undefined, +} + +/// Preserve null and explicit undefined params while adding provider selection to gateway login. +#[allow(dead_code)] +#[derive(TS)] +#[ts(untagged)] +enum GatewayOAuthLoginParamsTypeScript { + Params(Option), + #[ts(type = "undefined")] + Undefined, +} + client_request_definitions! { Initialize => "initialize" { params: v1::InitializeParams, @@ -1161,12 +1181,12 @@ client_request_definitions! { response: v2::ModelListResponse, }, GatewayOAuthRead => "account/gatewayOAuth/read" { - params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, + params: #[ts(optional, as = "Option", inline)] #[serde(default, skip_serializing_if = "Option::is_none")] v2::NullableGatewayOAuthReadParams, serialization: None, response: v2::GatewayOAuthReadResponse, }, GatewayOAuthLogin => "account/gatewayOAuth/login" { - params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, + params: #[ts(optional, as = "Option", inline)] #[serde(default, skip_serializing_if = "Option::is_none")] v2::NullableGatewayOAuthLoginParams, serialization: None, response: v2::GatewayOAuthLoginResponse, }, 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 a5a5009d12..de60aa91b6 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/account.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/account.rs @@ -652,6 +652,30 @@ impl std::fmt::Debug for GatewayOAuthChangedNotification { } } +/// Read credential readiness for a configured provider without starting browser login. +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct GatewayOAuthReadParams { + /// Defaults to the current provider when omitted. + #[ts(optional = nullable)] + pub model_provider: Option, +} + +pub type NullableGatewayOAuthReadParams = Option; + +/// Authorize a configured provider without changing the default or any thread's provider. +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct GatewayOAuthLoginParams { + /// Defaults to the current provider when omitted. + #[ts(optional = nullable)] + pub model_provider: Option, +} + +pub type NullableGatewayOAuthLoginParams = Option; + /// Current effective gateway policy and credential readiness; never contains credentials. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index fbea4730d4..ef6ad65b2a 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -648,7 +648,7 @@ cannot open a browser before initialization. Explicit opt-in is shared by gatewa 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`, +- `account/gatewayOAuth/read` returns the selected `providerId`, `providerName`, `required`, `status`, and `error`. `required` indicates that this provider uses gateway OAuth, including when already signed in. With a credential exporter, `required` is false and `status` is null. This read does not refresh @@ -668,14 +668,21 @@ cannot be undone by a later connection that omits the capability. 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 + `failed`, with an optional `error`. Notifications identify each configured + provider sharing the changed gateway credential, even when it is not the current + default. 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. +Read and login accept an optional `modelProvider`, for example +`{"modelProvider": "my_gateway"}`. Omitted params, null params, and an omitted or +null selector use the current provider. An explicit selector uses the provider definitions +loaded at startup and is checked against current managed provider policy before +credentials are read. Unknown or forbidden providers return an error instead of +falling back to the current provider. Login keeps the selected provider for the +entire attempt, even if the default changes. Cancel takes no params and cancels +the calling connection's active login, regardless of its provider. Status notifications include `providerId`. During browser sign-in, inference requests fail promptly and can be retried when sign-in succeeds. diff --git a/codex-rs/app-server/src/gateway_oauth_notifications.rs b/codex-rs/app-server/src/gateway_oauth_notifications.rs index 8002b6bfc0..4034e3f621 100644 --- a/codex-rs/app-server/src/gateway_oauth_notifications.rs +++ b/codex-rs/app-server/src/gateway_oauth_notifications.rs @@ -1,4 +1,4 @@ -//! Forward OAuth progress for the app's current gateway. +//! Forward OAuth progress for every configured provider sharing the changed grant. use crate::config_manager::ConfigManager; use crate::outgoing_message::OutgoingMessageSender; @@ -39,27 +39,30 @@ pub(crate) fn spawn( 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 !matches!(manager.oauth_session(), Some(oauth) if oauth.config() == &change.config) { - continue; + for (provider_id, provider) in &config.model_providers { + let Ok(Some(manager)) = codex_model_provider::create_model_provider( + provider.clone(), + Some(Arc::clone(&auth_manager)), + ) + .gateway_auth_manager() else { + continue; + }; + if !matches!(manager.oauth_session(), Some(oauth) if oauth.config() == &change.config) + { + continue; + } + let (status, error) = status_fields(change.status.clone()); + outgoing + .send_server_notification(ServerNotification::GatewayOAuthChanged( + GatewayOAuthChangedNotification { + auth_url: None, + provider_id: provider_id.clone(), + status, + error, + }, + )) + .await; } - 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/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index aada73793f..3fe14234c8 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -1890,12 +1890,13 @@ impl MessageProcessor { .bedrock_check_gov_cloud_requirements() .await } - ClientRequest::GatewayOAuthRead { .. } => { - Box::pin(self.account_processor.gateway_oauth_read()) - .await - .map(|response| Some(response.into())) - } - ClientRequest::GatewayOAuthLogin { .. } => { + ClientRequest::GatewayOAuthRead { params, .. } => Box::pin( + self.account_processor + .gateway_oauth_read(params.unwrap_or_default()), + ) + .await + .map(|response| Some(response.into())), + ClientRequest::GatewayOAuthLogin { params, .. } => { if session .opted_out_notification_methods() .contains("account/gatewayOAuth/changed") @@ -1904,10 +1905,11 @@ impl MessageProcessor { "Gateway login requires account/gatewayOAuth/changed notifications", )) } else { - Box::pin( - self.account_processor - .gateway_oauth_login(connection_id, &session.rpc_gate), - ) + Box::pin(self.account_processor.gateway_oauth_login( + params.unwrap_or_default(), + connection_id, + &session.rpc_gate, + )) .await .map(|response| Some(response.into())) } 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 index 7dd6c9ed81..7f9eb41c9c 100644 --- a/codex-rs/app-server/src/message_processor_gateway_oauth_tests.rs +++ b/codex-rs/app-server/src/message_processor_gateway_oauth_tests.rs @@ -24,6 +24,7 @@ use serde_json::json; use std::sync::Arc; use std::time::Duration; use tempfile::TempDir; +use test_case::test_case; use tokio::time::timeout; use wiremock::Mock; use wiremock::MockServer; @@ -47,8 +48,10 @@ async fn send_request( .await; } +#[test_case(false; "default provider")] +#[test_case(true; "explicit non-default provider")] #[tokio::test] -async fn gateway_oauth_login_updates_existing_requests() -> Result<()> { +async fn gateway_oauth_login_updates_existing_requests(selected: bool) -> Result<()> { let server = MockServer::start().await; Mock::given(method("POST")) .and(path("/token")) @@ -121,10 +124,23 @@ delivery = {{ kind = "header", name = "X-Gateway-Authorization" }} ) .await; let _: InitializeResponse = read_response(&mut outgoing, /*request_id*/ 1).await; + // Existing request consumers retain the gateway when the default changes. + if selected { + let path = home.path().join("config.toml"); + let text = std::fs::read_to_string(&path)?; + std::fs::write( + path, + text.replace( + "model_provider = \"gateway\"", + "model_provider = \"openai\"", + ), + )?; + } + let params = selected.then(|| json!({"modelProvider": "gateway"})); send_request( &processor, &session, - json!({"id": 2, "method": "account/gatewayOAuth/login"}), + json!({"id": 2, "method": "account/gatewayOAuth/login", "params": params}), ) .await; let authorization_url = timeout(Duration::from_secs(/*secs*/ 10), async { @@ -166,7 +182,7 @@ delivery = {{ kind = "header", name = "X-Gateway-Authorization" }} send_request( &processor, &session, - json!({"id": 3, "method": "account/gatewayOAuth/read"}), + json!({"id": 3, "method": "account/gatewayOAuth/read", "params": params}), ) .await; let response: GatewayOAuthReadResponse = read_response(&mut outgoing, /*request_id*/ 3).await; @@ -288,3 +304,61 @@ delivery = {{ kind = "header", name = "X-Gateway-Authorization" }} processor.drain_background_tasks().await; Ok(()) } + +/// Checking another provider must not recreate the first provider's readiness state. +#[tokio::test] +async fn gateway_oauth_read_retains_each_provider_status() -> Result<()> { + let home = TempDir::new()?; + let mut text = String::from("[features]\nsecret_auth_storage = true\n"); + for id in ["first", "second"] { + text.push_str(&format!( + r#" +[model_providers.{id}] +name = "{id}" +base_url = "https://{id}.example/v1" +wire_api = "responses" +[model_providers.{id}.gateway_oauth] +authorization_url = "https://{id}.example/authorize" +token_url = "https://{id}.example/token" +client_id = "{id}" +delivery = {{ kind = "header", name = "X-Gateway-Authorization" }} +"# + )); + } + std::fs::write(home.path().join("config.toml"), text)?; + let config = Arc::new( + ConfigBuilder::default() + .codex_home(home.path().to_path_buf()) + .build() + .await?, + ); + let auth = AuthManager::from_auth_for_testing_with_home( + CodexAuth::from_api_key("primary"), + home.path().to_path_buf(), + ); + let mut changes = codex_login::subscribe_gateway_auth_status(&auth.runtime_config()); + let (processor, _outgoing) = build_test_processor(config, auth).await; + for (id, changed) in [("first", true), ("second", true), ("first", false)] { + let state = processor + .account_processor + .gateway_oauth_read(codex_app_server_protocol::GatewayOAuthReadParams { + model_provider: Some(id.into()), + }) + .await + .map_err(|err| anyhow::anyhow!("{}", err.message))?; + assert_eq!(state.provider_id, id); + assert_eq!(state.status, Some(GatewayOAuthStatus::NotReady)); + if changed { + assert_eq!( + changes.try_recv()?.status, + codex_login::GatewayAuthStatus::NotReady + ); + } else { + assert!(matches!( + changes.try_recv(), + Err(tokio::sync::broadcast::error::TryRecvError::Empty) + )); + } + } + Ok(()) +} 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 8f75f86d07..178edfc5ae 100644 --- a/codex-rs/app-server/src/request_processors/account_processor.rs +++ b/codex-rs/app-server/src/request_processors/account_processor.rs @@ -117,7 +117,8 @@ pub(crate) struct AccountRequestProcessor { workspace_routing_fetches: Arc>, workspace_routing_shutdown: CancellationToken, gateway_login: Arc>>, - gateway_client: Arc>>>, + gateway_clients: + Arc>>>, _gateway_notifications: Arc>, pub(super) enterprise_login: Arc, } @@ -151,7 +152,7 @@ impl AccountRequestProcessor { model_catalog, 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)), + gateway_clients: Arc::new(std::sync::Mutex::new(HashMap::new())), workspace_routing: Arc::new(Mutex::new(None)), workspace_routing_fetches: Arc::new(Mutex::new(HashMap::new())), workspace_routing_shutdown: CancellationToken::new(), 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 index 7899fcb0f5..d06c7d5592 100644 --- 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 @@ -30,14 +30,25 @@ impl Drop for LoginGuard { 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}")) - })?; + model_provider: Option, + ) -> Result<(Arc, Option>), JSONRPCErrorError> { + let config = match model_provider.as_deref() { + Some(provider_id) => { + let config = config_for_provider(&self.config, provider_id)?; + // Explicit selectors use startup definitions, so validate against current policy. + self.config_manager + .check_thread_model_provider(&config) + .await + .map_err(|err| config_load_error(&err))?; + config + } + None => Arc::new( + self.config_manager + .load_latest_config(/*fallback_cwd*/ None) + .await + .map_err(|err| config_load_error(&err))?, + ), + }; let manager = codex_model_provider::create_model_provider( config.model_provider.clone(), Some(Arc::clone(&self.auth_manager)), @@ -47,17 +58,21 @@ impl AccountRequestProcessor { let session = manager .as_deref() .and_then(GatewayAuthManager::oauth_session); - *self - .gateway_client - .lock() - .unwrap_or_else(PoisonError::into_inner) = manager; + // Keep sign-in status alive when clients alternate between providers. + if let Some(manager) = &manager { + self.gateway_clients + .lock() + .unwrap_or_else(PoisonError::into_inner) + .insert(config.model_provider_id.clone(), Arc::clone(manager)); + } Ok((config, session)) } pub(crate) async fn gateway_oauth_read( &self, + params: codex_app_server_protocol::GatewayOAuthReadParams, ) -> Result { - let (config, client) = self.gateway_client().await?; + let (config, client) = self.gateway_client(params.model_provider).await?; let state = if let Some(client) = client { Some(status_fields( client @@ -69,8 +84,8 @@ impl AccountRequestProcessor { None }; Ok(GatewayOAuthReadResponse { - provider_id: config.model_provider_id, - provider_name: config.model_provider.name, + provider_id: config.model_provider_id.clone(), + provider_name: config.model_provider.name.clone(), required: state.is_some(), status: state.as_ref().map(|state| state.0), error: state.and_then(|state| state.1), @@ -79,6 +94,7 @@ impl AccountRequestProcessor { pub(crate) async fn gateway_oauth_login( &self, + params: codex_app_server_protocol::GatewayOAuthLoginParams, owner: ConnectionId, connection_gate: &crate::connection_rpc_gate::ConnectionRpcGate, ) -> Result { @@ -101,9 +117,9 @@ impl AccountRequestProcessor { }); LoginGuard(Arc::clone(&self.gateway_login)) }; - let (config, client) = self.gateway_client().await?; + let (config, client) = self.gateway_client(params.model_provider).await?; let client = client - .ok_or_else(|| invalid_request("The current provider does not use gateway OAuth"))?; + .ok_or_else(|| invalid_request("The selected 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() 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 index 9ad6995171..4193b65c20 100644 --- a/codex-rs/app-server/tests/suite/v2/gateway_oauth_tests.rs +++ b/codex-rs/app-server/tests/suite/v2/gateway_oauth_tests.rs @@ -401,7 +401,7 @@ async fn read_and_cancel_without_gateway_oauth() -> Result<()> { .await??; assert_eq!( error.error.message, - "The current provider does not use gateway OAuth" + "The selected provider does not use gateway OAuth" ); Ok(()) } @@ -686,3 +686,54 @@ async fn initialization_selects_legacy_or_explicit_gateway_login() -> Result<()> } Ok(()) } + +/// Non-default grants are readable and notify every configured alias without starting login. +#[tokio::test] +async fn selected_gateway_read_notifies_matching_providers() -> Result<()> { + let home = TempDir::new()?; + let oauth = MockServer::start().await; + let config = gateway_config(&oauth).replace( + "model_provider = \"gateway\"", + "model_provider = \"openai\"", + ); + let alias = config[config.find("[model_providers.gateway]").unwrap()..] + .replace("model_providers.gateway", "model_providers.alias"); + std::fs::write( + home.path().join("config.toml"), + format!("{config}\n{alias}"), + )?; + let mut server = gateway_server(home.path()).await?; + assert!(!read_gateway(&mut server).await?.required); + let id = server + .send_raw_request( + "account/gatewayOAuth/read", + Some(serde_json::json!({"modelProvider": "gateway"})), + ) + .await?; + let state: GatewayOAuthReadResponse = server.read_response(id).await?; + assert_eq!( + state, + GatewayOAuthReadResponse { + provider_id: "gateway".into(), + provider_name: "Test gateway".into(), + required: true, + status: Some(GatewayOAuthStatus::NotReady), + error: None, + } + ); + let mut providers = Vec::new(); + for _ in 0..2 { + let change: GatewayOAuthChangedNotification = timeout( + TIMEOUT, + server.read_notification("account/gatewayOAuth/changed"), + ) + .await??; + assert_eq!(change.status, GatewayOAuthStatus::NotReady); + assert_eq!(change.auth_url, None); + providers.push(change.provider_id); + } + providers.sort(); + assert_eq!(providers, ["alias", "gateway"]); + assert!(oauth.received_requests().await.unwrap().is_empty()); + Ok(()) +} diff --git a/sdk/python/src/openai_codex/generated/v2_all.py b/sdk/python/src/openai_codex/generated/v2_all.py index 56d9df8b6b..807a4d3a96 100644 --- a/sdk/python/src/openai_codex/generated/v2_all.py +++ b/sdk/python/src/openai_codex/generated/v2_all.py @@ -1842,12 +1842,32 @@ class GatewayOAuthCancelResponse(BaseModel): ) +class GatewayOAuthLoginParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + model_provider: Annotated[ + str | None, + Field(alias="modelProvider", description="Defaults to the current provider when omitted."), + ] = None + + class GatewayOAuthLoginResponse(BaseModel): model_config = ConfigDict( populate_by_name=True, ) +class GatewayOAuthReadParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + model_provider: Annotated[ + str | None, + Field(alias="modelProvider", description="Defaults to the current provider when omitted."), + ] = None + + class GatewayOAuthStatus(Enum): not_ready = "notReady" started = "started" @@ -3086,6 +3106,14 @@ class NonSteerableTurnKind(Enum): compact = "compact" +class NullableGatewayOAuthLoginParams(RootModel[GatewayOAuthLoginParams | None]): + root: Annotated[GatewayOAuthLoginParams | None, Field(title="Nullable_GatewayOAuthLoginParams")] + + +class NullableGatewayOAuthReadParams(RootModel[GatewayOAuthReadParams | None]): + root: Annotated[GatewayOAuthReadParams | None, Field(title="Nullable_GatewayOAuthReadParams")] + + class NullableGetAccountRateLimitsParams(RootModel[GetAccountRateLimitsParams | None]): root: Annotated[ GetAccountRateLimitsParams | None, Field(title="Nullable_GetAccountRateLimitsParams") @@ -7956,7 +7984,7 @@ class AccountGatewayOAuthReadRequest(BaseModel): method: Annotated[ Literal["account/gatewayOAuth/read"], Field(title="Account/gatewayOAuth/readRequestMethod") ] - params: Annotated[None, Field(None)] = None + params: GatewayOAuthReadParams | None = None class AccountGatewayOAuthLoginRequest(BaseModel): @@ -7968,7 +7996,7 @@ class AccountGatewayOAuthLoginRequest(BaseModel): Literal["account/gatewayOAuth/login"], Field(title="Account/gatewayOAuth/loginRequestMethod"), ] - params: Annotated[None, Field(None)] = None + params: GatewayOAuthLoginParams | None = None class AccountGatewayOAuthCancelRequest(BaseModel):