diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 1d7177e067..f5ad15c2c4 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -8662,6 +8662,29 @@ "title": "Account/logoutRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/securitySetup/read" + ], + "title": "Account/securitySetup/readRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Account/securitySetup/readRequest", + "type": "object" + }, { "properties": { "id": { 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 6a4c723bd9..5613fc01d1 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 @@ -2301,6 +2301,29 @@ "title": "Account/logoutRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "account/securitySetup/read" + ], + "title": "Account/securitySetup/readRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Account/securitySetup/readRequest", + "type": "object" + }, { "properties": { "id": { @@ -7016,6 +7039,48 @@ ], "type": "string" }, + "AccountSecuritySetupAction": { + "properties": { + "label": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "label", + "url" + ], + "type": "object" + }, + "AccountSecuritySetupNotice": { + "properties": { + "accountId": { + "type": "string" + }, + "action": { + "$ref": "#/definitions/v2/AccountSecuritySetupAction" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "required": [ + "accountId", + "action", + "description", + "title", + "userId" + ], + "type": "object" + }, "AccountSession": { "properties": { "displayName": { @@ -13399,6 +13464,24 @@ "title": "GetAccountResponse", "type": "object" }, + "GetAccountSecuritySetupResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Optional security reminder bound to the account that fetched it. Unavailable or ineligible accounts return no notice; this read never changes account security.", + "properties": { + "notice": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AccountSecuritySetupNotice" + }, + { + "type": "null" + } + ] + } + }, + "title": "GetAccountSecuritySetupResponse", + "type": "object" + }, "GetAccountTokenUsageParams": { "properties": { "threadId": { 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 26154496d2..60e5af98af 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 @@ -154,6 +154,48 @@ ], "type": "string" }, + "AccountSecuritySetupAction": { + "properties": { + "label": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "label", + "url" + ], + "type": "object" + }, + "AccountSecuritySetupNotice": { + "properties": { + "accountId": { + "type": "string" + }, + "action": { + "$ref": "#/definitions/AccountSecuritySetupAction" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "required": [ + "accountId", + "action", + "description", + "title", + "userId" + ], + "type": "object" + }, "AccountSession": { "properties": { "displayName": { @@ -4056,6 +4098,29 @@ "title": "Account/logoutRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/securitySetup/read" + ], + "title": "Account/securitySetup/readRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Account/securitySetup/readRequest", + "type": "object" + }, { "properties": { "id": { @@ -9291,6 +9356,24 @@ "title": "GetAccountResponse", "type": "object" }, + "GetAccountSecuritySetupResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Optional security reminder bound to the account that fetched it. Unavailable or ineligible accounts return no notice; this read never changes account security.", + "properties": { + "notice": { + "anyOf": [ + { + "$ref": "#/definitions/AccountSecuritySetupNotice" + }, + { + "type": "null" + } + ] + } + }, + "title": "GetAccountSecuritySetupResponse", + "type": "object" + }, "GetAccountTokenUsageParams": { "properties": { "threadId": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/GetAccountSecuritySetupResponse.json b/codex-rs/app-server-protocol/schema/json/v2/GetAccountSecuritySetupResponse.json new file mode 100644 index 0000000000..c8a4d73435 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/GetAccountSecuritySetupResponse.json @@ -0,0 +1,62 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AccountSecuritySetupAction": { + "properties": { + "label": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "label", + "url" + ], + "type": "object" + }, + "AccountSecuritySetupNotice": { + "properties": { + "accountId": { + "type": "string" + }, + "action": { + "$ref": "#/definitions/AccountSecuritySetupAction" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "required": [ + "accountId", + "action", + "description", + "title", + "userId" + ], + "type": "object" + } + }, + "description": "Optional security reminder bound to the account that fetched it. Unavailable or ineligible accounts return no notice; this read never changes account security.", + "properties": { + "notice": { + "anyOf": [ + { + "$ref": "#/definitions/AccountSecuritySetupNotice" + }, + { + "type": "null" + } + ] + } + }, + "title": "GetAccountSecuritySetupResponse", + "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 a1d1ca3642..85d45d7da6 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 47a33c0d68..ebdb768d80 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 be2e11da52..de12ebd404 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -110,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?: 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, }; +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/securitySetup/read", 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/AccountSecuritySetupAction.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AccountSecuritySetupAction.ts new file mode 100644 index 0000000000..e56ddeb485 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AccountSecuritySetupAction.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 AccountSecuritySetupAction = { label: string, url: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AccountSecuritySetupNotice.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AccountSecuritySetupNotice.ts new file mode 100644 index 0000000000..81e54807f9 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AccountSecuritySetupNotice.ts @@ -0,0 +1,6 @@ +// 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 { AccountSecuritySetupAction } from "./AccountSecuritySetupAction"; + +export type AccountSecuritySetupNotice = { accountId: string, userId: string, title: string, description: string, action: AccountSecuritySetupAction, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/GetAccountSecuritySetupResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GetAccountSecuritySetupResponse.ts new file mode 100644 index 0000000000..605c3f008a --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/GetAccountSecuritySetupResponse.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 { AccountSecuritySetupNotice } from "./AccountSecuritySetupNotice"; + +/** + * Optional security reminder bound to the account that fetched it. Unavailable or + * ineligible accounts return no notice; this read never changes account security. + */ +export type GetAccountSecuritySetupResponse = { notice: AccountSecuritySetupNotice | 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 6f98f4556d..a648367933 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -4,6 +4,8 @@ export type { Account } from "./Account"; export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedNotification"; export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification"; export type { AccountRoutingOverride } from "./AccountRoutingOverride"; +export type { AccountSecuritySetupAction } from "./AccountSecuritySetupAction"; +export type { AccountSecuritySetupNotice } from "./AccountSecuritySetupNotice"; export type { AccountSession } from "./AccountSession"; export type { AccountSessionWorkspace } from "./AccountSessionWorkspace"; export type { AccountSessionWorkspaceKind } from "./AccountSessionWorkspaceKind"; @@ -220,6 +222,7 @@ export type { GetAccountParams } from "./GetAccountParams"; export type { GetAccountRateLimitsParams } from "./GetAccountRateLimitsParams"; export type { GetAccountRateLimitsResponse } from "./GetAccountRateLimitsResponse"; export type { GetAccountResponse } from "./GetAccountResponse"; +export type { GetAccountSecuritySetupResponse } from "./GetAccountSecuritySetupResponse"; export type { GetAccountTokenUsageParams } from "./GetAccountTokenUsageParams"; export type { GetAccountTokenUsageResponse } from "./GetAccountTokenUsageResponse"; export type { GetWorkspaceMessagesResponse } from "./GetWorkspaceMessagesResponse"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 8da1a8a4b8..d61e1b3c72 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1421,6 +1421,12 @@ client_request_definitions! { response: v2::LogoutAccountResponse, }, + GetAccountSecuritySetup => "account/securitySetup/read" { + params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, + serialization: None, + response: v2::GetAccountSecuritySetupResponse, + }, + GetAccountRateLimits => "account/rateLimits/read" { params: #[ts(optional, as = "Option", inline)] #[serde(default, skip_serializing_if = "Option::is_none")] v2::NullableGetAccountRateLimitsParams, serialization: None, 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 b01d01641e..bae18f3b92 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/account.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/account.rs @@ -884,3 +884,31 @@ pub struct AccountLoginCompletedNotification { pub enum DesktopOnboardingEntrypoint { LifeSciences, } + +/// Optional security reminder bound to the account that fetched it. Unavailable or +/// ineligible accounts return no notice; this read never changes account security. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct GetAccountSecuritySetupResponse { + pub notice: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AccountSecuritySetupNotice { + pub account_id: String, + pub user_id: String, + pub title: String, + pub description: String, + pub action: AccountSecuritySetupAction, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AccountSecuritySetupAction { + pub label: String, + pub url: String, +} diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index b36a9d2a9d..6539b7f6b5 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -587,6 +587,8 @@ PAT sessions continue to return no bearer token from `getAuthStatus`. # Selected workspace routing +`account/securitySetup/read` returns an optional backend-owned security reminder for the saved ChatGPT login. A returned notice includes its account and user IDs; clients must discard it after an account change and validate its copy and action URL before rendering. Ineligible, unavailable, and timed-out reads return `notice: null`. Older app-servers may not support this optional read. + The experimental `account/read.workspaceRouting` response field returns the selected ChatGPT workspace's `chatgptAccountId`, resolved HTTPS `backendOrigin`, and backend-provided `accountRoutingOverride`. The routing value is `us`, `us_cr`, or the explicit `NO_CONSTRAINT` value. API-only and signed-out accounts return `null` and do not need `accounts/check`. App-server discovers routing for saved ChatGPT logins at startup and for new logins or workspace switches. After requirements and routing are ready, it sends the existing `account/updated` notification. Newly initialized connections also receive this notification once saved-workspace routing is ready, including when discovery finished before the connection initialized. Clients then reread `configRequirements/read` and `account/read`. Saved ChatGPT credentials without a selected workspace ID retain their account information and return `workspaceRouting: null`; app-server does not guess a workspace from the backend's default account. Discovery failures for a selected workspace, including missing or null fields from older backends, return an `account/read` error. They never produce a successful unrestricted result. A later read retries failed discovery. Logout clears the cached routing, and results from earlier authentication owners are discarded. Token refreshes for the same known user and workspace invalidate cached routing without cancelling discovery or failing sign-in. Configuration is reloaded after discovery; a changed backend, model provider, or required backend rejects the result so the next read discovers against current configuration. Account notifications recheck the auth owner generation after waiting for outbound queue capacity. Superseded sign-in attempts emit a failed `account/login/completed` event instead of silently dropping completion. Notifications remain snapshots: clients reread current account and requirements state rather than treating a queued notification as authorization. diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 54ea37f428..309d579403 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -1942,6 +1942,12 @@ impl MessageProcessor { ClientRequest::GetAuthStatus { params, .. } => { self.account_processor.get_auth_status(params).await } + ClientRequest::GetAccountSecuritySetup { .. } => Ok(Some( + self.account_processor + .get_account_security_setup() + .await + .into(), + )), ClientRequest::GetAccountRateLimits { params, .. } => { self.account_processor.get_account_rate_limits(params).await } 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 ba8672181c..f85b6bcd92 100644 --- a/codex-rs/app-server/src/request_processors/account_processor.rs +++ b/codex-rs/app-server/src/request_processors/account_processor.rs @@ -24,6 +24,7 @@ mod enterprise_login; mod gateway_oauth; mod gcp_setup; mod rate_limit_resets; +mod security_setup; mod workspace_routing; mod workspace_switching; diff --git a/codex-rs/app-server/src/request_processors/account_processor/security_setup.rs b/codex-rs/app-server/src/request_processors/account_processor/security_setup.rs new file mode 100644 index 0000000000..d3c681ab5e --- /dev/null +++ b/codex-rs/app-server/src/request_processors/account_processor/security_setup.rs @@ -0,0 +1,115 @@ +//! Keep optional security reminders on the app-server's existing runtime identity. +//! Only saved ChatGPT credentials matching the active login may fetch a notice. + +use super::*; +use codex_app_server_protocol::AccountSecuritySetupAction; +use codex_app_server_protocol::AccountSecuritySetupNotice; +use codex_app_server_protocol::GetAccountSecuritySetupResponse; +use codex_http_client::ClientRouteClass; +use codex_http_client::RouteAwareClientPool; +use serde::Deserialize; + +#[derive(Deserialize)] +struct Response { + notice: Option, +} + +#[derive(Deserialize)] +struct Notice { + title: String, + description: String, + action: AccountSecuritySetupAction, +} + +impl AccountRequestProcessor { + pub(crate) async fn get_account_security_setup(&self) -> GetAccountSecuritySetupResponse { + let notice = tokio::time::timeout(Duration::from_secs(/*secs*/ 3), async { + let config = self.load_latest_config().await; + let (auth, factory) = self.auth_manager.auth_with_http_client_factory().await?; + let source_generation = self + .auth_manager + .auth_change_state_for(&auth)? + .source_generation; + let saved = config + .auth_config() + .load_auth(/*enable_codex_api_key_env*/ false) + .await + .ok() + .flatten()?; + if !matches!(saved, CodexAuth::Chatgpt(_)) || saved.is_fedramp_account() { + return None; + } + if auth.auth_mode() != codex_protocol::auth::AuthMode::Chatgpt + || saved.get_token().ok()? != auth.get_token().ok()? + { + return None; + } + let account_id = auth.get_account_id()?; + let user_id = auth.get_chatgpt_user_id()?; + let url = format!( + "{}/wham/security-setup", + config.chatgpt_base_url.trim_end_matches('/') + ); + let runtime = self + .thread_manager + .runtime_auth_for_config(&config) + .ok()? + .filter(|_| codex_runtime_agent_instance::is_openai_api_url(&url)); + let resolved = codex_model_provider::auth_provider_for_request( + &auth, + runtime.as_ref(), + /*headers*/ &Default::default(), + ) + .await + .ok()?; + let factory = resolved.bind_http_client_factory(factory).ok()?; + let client = + RouteAwareClientPool::new_without_redirects(factory, ClientRouteClass::Api); + let mut headers = resolved + .auth + .apply_auth_headers(Default::default()) + .await + .ok()?; + headers.insert("chatgpt-account-id", account_id.parse().ok()?); + resolved.validate_headers(&headers).ok()?; + let response = client + .get(&url) + .headers(headers) + .header( + "User-Agent", + codex_login::default_client::get_codex_user_agent(), + ) + .send() + .await + .ok()?; + if !response.status().is_success() { + return None; + } + let notice = response.json::().await.ok()?.notice?; + let current = self.auth_manager.auth().await?; + if self + .auth_manager + .auth_change_state_for(&auth)? + .source_generation + != source_generation + || current.auth_mode() != codex_protocol::auth::AuthMode::Chatgpt + || current.get_account_id().as_ref() != Some(&account_id) + || current.get_chatgpt_user_id().as_ref() != Some(&user_id) + || current.is_fedramp_account() + { + return None; + } + Some(AccountSecuritySetupNotice { + account_id, + user_id, + title: notice.title, + description: notice.description, + action: notice.action, + }) + }) + .await + .ok() + .flatten(); + GetAccountSecuritySetupResponse { notice } + } +} diff --git a/codex-rs/app-server/tests/suite/v2/rate_limits.rs b/codex-rs/app-server/tests/suite/v2/rate_limits.rs index 122d44c396..fa9c341330 100644 --- a/codex-rs/app-server/tests/suite/v2/rate_limits.rs +++ b/codex-rs/app-server/tests/suite/v2/rate_limits.rs @@ -843,6 +843,24 @@ async fn account_requests_share_runtime_and_rotate_on_restart(mode: &str) -> Res let apps: codex_app_server_protocol::AppsReadResponse = timeout(DEFAULT_READ_TIMEOUT, app.read_response(id)).await??; assert_eq!(apps.apps[0].id, "details-only"); + let id = app + .send_request("account/securitySetup/read", /*params*/ None) + .await?; + let response: codex_app_server_protocol::GetAccountSecuritySetupResponse = + timeout(DEFAULT_READ_TIMEOUT, app.read_response(id)).await??; + assert_eq!( + response.notice, + Some(codex_app_server_protocol::AccountSecuritySetupNotice { + account_id: "account-123".into(), + user_id: "user-123".into(), + title: "Set up security".into(), + description: "Account security reminder".into(), + action: codex_app_server_protocol::AccountSecuritySetupAction { + label: "Settings".into(), + url: "https://chatgpt.com/cyber".into(), + }, + }) + ); } assert_eq!(std::fs::read(home.path().join("auth.json"))?, before); return Ok(()); @@ -892,6 +910,17 @@ async fn account_requests_share_runtime_and_rotate_on_restart(mode: &str) -> Res }))) .mount(&server) .await; + Mock::given(method("GET")) + .and(path("/backend-api/wham/security-setup")) + .respond_with( + ResponseTemplate::new(/*s*/ 200).set_body_json(json!({"notice": { + "title": "Set up security", "description": "Account security reminder", + "action": {"label": "Settings", "url": "https://chatgpt.com/cyber"}, + }})), + ) + .expect(/*r*/ 2) + .mount(&server) + .await; run_harness_through_proxy( &server, &format!( @@ -987,3 +1016,80 @@ async fn account_requests_share_runtime_and_rotate_on_restart(mode: &str) -> Res server.verify().await; Ok(()) } + +#[test_case("api_key")] +#[test_case("fedramp")] +#[test_case("external_token")] +#[test_case("signed_out")] +#[tokio::test] +async fn security_setup_requires_matching_saved_chatgpt_login(kind: &str) -> Result<()> { + use base64::Engine; + let token = format!("e30.{}.c2ln", base64::engine::general_purpose::URL_SAFE_NO_PAD.encode( + json!({"exp": chrono::Utc::now().timestamp() + 3600, + "https://api.openai.com/auth": {"chatgpt_user_id": "user-123", "chatgpt_account_id": "account-123"}}).to_string())); + let home = TempDir::new()?; + let server = MockServer::start().await; + write_chatgpt_base_url(home.path(), &format!("{}/backend-api", server.uri()))?; + if kind != "signed_out" { + write_chatgpt_auth( + home.path(), + ChatGptAuthFixture::new(&token) + .account_id("account-123") + .claims(ChatGptIdTokenClaims { + chatgpt_account_id: Some("account-123".into()), + chatgpt_user_id: Some("user-123".into()), + chatgpt_account_is_fedramp: kind == "fedramp", + ..Default::default() + }), + AuthCredentialsStoreMode::File, + )?; + } + app_test_support::mount_workspace_routing(&server).await; + Mock::given(method("GET")) + .and(path("/backend-api/wham/security-setup")) + .respond_with(ResponseTemplate::new(/*s*/ 200).set_body_json(json!({"notice": null}))) + .expect(/*r*/ 0) + .mount(&server) + .await; + let mut app = TestAppServer::builder() + .with_codex_home(home.path()) + .without_auto_env() + .with_env_overrides(&[("OPENAI_API_KEY", None)]) + .build_initialized_with_timeout(DEFAULT_READ_TIMEOUT) + .await?; + if kind == "api_key" { + let id = app + .send_request( + "account/login/start", + Some(json!({ + "type": "apiKey", "apiKey": "selected-api-key", + })), + ) + .await?; + let response: serde_json::Value = + timeout(DEFAULT_READ_TIMEOUT, app.read_response(id)).await??; + assert_eq!(response["type"], "apiKey"); + } + if kind == "external_token" { + let id = app + .send_request( + "account/login/start", + Some(json!({ + "type": "chatgptAuthTokens", "accessToken": token, + "chatgptAccountId": "account-123", "chatgptPlanType": "pro", + })), + ) + .await?; + let response: serde_json::Value = + timeout(DEFAULT_READ_TIMEOUT, app.read_response(id)).await??; + assert_eq!(response["type"], "chatgptAuthTokens"); + } + let id = app + .send_request("account/securitySetup/read", /*params*/ None) + .await?; + let response: codex_app_server_protocol::GetAccountSecuritySetupResponse = + timeout(DEFAULT_READ_TIMEOUT, app.read_response(id)).await??; + assert_eq!(response.notice, None); + server.verify().await; + Ok(()) +} diff --git a/codex-rs/app-server/tests/suite/v2/rate_limits_identity_tests.rs b/codex-rs/app-server/tests/suite/v2/rate_limits_identity_tests.rs index 635ebfad03..f112568588 100644 --- a/codex-rs/app-server/tests/suite/v2/rate_limits_identity_tests.rs +++ b/codex-rs/app-server/tests/suite/v2/rate_limits_identity_tests.rs @@ -140,3 +140,97 @@ async fn identity_is_rechecked_after_backend_response( assert_eq!(response, expected); Ok(()) } + +#[test_case(false; "same_login")] +#[test_case(true; "same_user_external_login")] +#[tokio::test] +async fn security_setup_rechecks_login_mode_after_backend_response( + change_login: bool, +) -> Result<()> { + use codex_app_server_protocol::GetAccountSecuritySetupResponse; + + let token = encode_id_token( + &ChatGptIdTokenClaims::new() + .chatgpt_account_id("workspace-a") + .chatgpt_user_id("user-a"), + )?; + let home = TempDir::new()?; + write_chatgpt_auth( + home.path(), + ChatGptAuthFixture::new(&token) + .account_id("workspace-a") + .chatgpt_user_id("user-a") + .plan_type("team"), + AuthCredentialsStoreMode::File, + )?; + let entered = Arc::new(Notify::new()); + let release = Arc::new(Notify::new()); + let request_entered = Arc::clone(&entered); + let response_release = Arc::clone(&release); + let listener = TcpListener::bind("127.0.0.1:0").await?; + std::fs::write( + home.path().join("config.toml"), + format!( + "chatgpt_base_url = \"http://{}/backend-api\"\n", + listener.local_addr()? + ), + )?; + let expected_authorization = format!("Bearer {token}"); + let router = Router::new().route( + "/backend-api/wham/security-setup", + get(move |headers: HeaderMap| { + let expected_authorization = expected_authorization.clone(); + let entered = Arc::clone(&request_entered); + let release = Arc::clone(&response_release); + async move { + assert_eq!(headers["authorization"], expected_authorization); + assert_eq!(headers["chatgpt-account-id"], "workspace-a"); + entered.notify_one(); + release.notified().await; + Json(json!({"notice": { + "title": "Set up security", + "description": "Account security reminder", + "action": {"label": "Settings", "url": "https://chatgpt.com/cyber"} + }})) + } + }), + ); + let server = tokio::spawn(async move { axum::serve(listener, router).await }); + let mut app = TestAppServer::builder() + .with_codex_home(home.path()) + .without_auto_env() + .with_env_overrides(&[("OPENAI_API_KEY", None)]) + .build_initialized_with_timeout(READ_TIMEOUT) + .await?; + let request = app + .send_request("account/securitySetup/read", /*params*/ None) + .await?; + timeout(READ_TIMEOUT, entered.notified()).await?; + if change_login { + let login = app + .send_chatgpt_auth_tokens_login_request( + token, + "workspace-a".into(), + Some("team".into()), + ) + .await?; + let response: LoginAccountResponse = + timeout(READ_TIMEOUT, app.read_response(login)).await??; + assert_eq!(response, LoginAccountResponse::ChatgptAuthTokens {}); + } + release.notify_one(); + let response: GetAccountSecuritySetupResponse = + timeout(READ_TIMEOUT, app.read_response(request)).await??; + server.abort(); + if change_login { + assert_eq!(response.notice, None); + } else { + let notice = response + .notice + .expect("unchanged saved login gets its notice"); + assert_eq!(notice.account_id, "workspace-a"); + assert_eq!(notice.user_id, "user-a"); + assert_eq!(notice.title, "Set up security"); + } + Ok(()) +} diff --git a/codex-rs/tui/src/app/tests/security_setup_tests.rs b/codex-rs/tui/src/app/tests/security_setup_tests.rs index ba3f805acb..1e2f9be518 100644 --- a/codex-rs/tui/src/app/tests/security_setup_tests.rs +++ b/codex-rs/tui/src/app/tests/security_setup_tests.rs @@ -193,16 +193,15 @@ async fn security_setup_fetch_with_default_features_uses_authenticated_codex_end } #[tokio::test] -async fn security_setup_skips_fetch_when_server_auth_does_not_match_saved_login() -> Result<()> { - for (auth_method, auth_token) in [ - ( - Some(AuthMode::ChatgptAuthTokens), - Some("other-account-token"), - ), - (Some(AuthMode::ChatgptAuthTokens), Some("saved-token")), - (Some(AuthMode::Chatgpt), Some("other-account-token")), - (Some(AuthMode::Chatgpt), None), - (None, None), +async fn security_setup_skips_unavailable_or_mismatched_server_notice() -> Result<()> { + for response in [ + json!({"result": {"notice": null}}), + json!({"error": {"code": -32601, "message": "Method not found"}}), + json!({"result": {"notice": { + "accountId": "other-account", "userId": "other-user", + "title": "Keep using Daybreak mode", "description": "Set up security.", + "action": {"label": "Set up security", "url": "https://chatgpt.com/cyber"} + }}}), ] { let (mut app, _events, _ops) = make_test_app_with_channels().await; let backend = wiremock::MockServer::start().await; @@ -232,15 +231,8 @@ async fn security_setup_skips_fetch_when_server_auth_does_not_match_saved_login( let daemon = tokio::spawn(async move { let (stream, _) = listener.accept().await?; serve_reconnect_requests(tokio_tungstenite::accept_async(stream).await?, |request| { - assert_eq!(request.method, "getAuthStatus"); - assert_eq!( - request.params, - Some(json!({"includeToken": true, "refreshToken": false})) - ); - std::future::ready(Some(json!({"result": { - "authMethod": auth_method, "authToken": auth_token, - "requiresOpenaiAuth": true - }}))) + assert_eq!(request.method, "account/securitySetup/read"); + std::future::ready(Some(response.clone())) }) .await }); @@ -265,7 +257,11 @@ async fn security_setup_skips_fetch_when_server_auth_does_not_match_saved_login( backend.verify().await; server.shutdown().await?; let methods = daemon.await??; - assert!(methods.iter().any(|method| method == "getAuthStatus")); + assert!( + methods + .iter() + .any(|method| method == "account/securitySetup/read") + ); } Ok(()) } diff --git a/codex-rs/tui/src/security_setup.rs b/codex-rs/tui/src/security_setup.rs index 62e7a7c3d6..5c7ce78969 100644 --- a/codex-rs/tui/src/security_setup.rs +++ b/codex-rs/tui/src/security_setup.rs @@ -6,13 +6,9 @@ use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; use crate::app_server_session::AppServerSession; use crate::legacy_core::config::Config; -use codex_app_server_protocol::AuthMode; use codex_app_server_protocol::ClientRequest; -use codex_app_server_protocol::GetAuthStatusParams; -use codex_app_server_protocol::GetAuthStatusResponse; +use codex_app_server_protocol::GetAccountSecuritySetupResponse; use codex_app_server_protocol::RequestId; -use codex_http_client::ClientRouteClass; -use codex_http_client::RouteAwareClientPool; use codex_login::CodexAuth; use serde::Deserialize; use std::time::Duration; @@ -31,10 +27,6 @@ pub(crate) struct Action { pub url: String, } -#[derive(Deserialize)] -struct Response { - notice: Option, -} #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct Identity { pub account: String, @@ -81,58 +73,26 @@ pub(crate) fn prefetch( let request_handle = server.request_handle(); tokio::spawn(async move { let result = tokio::time::timeout(Duration::from_secs(3), async { - // Saved credentials must match the connected server, which may use external auth. - let status: GetAuthStatusResponse = request_handle - .request_typed(ClientRequest::GetAuthStatus { + let response: GetAccountSecuritySetupResponse = request_handle + .request_typed(ClientRequest::GetAccountSecuritySetup { request_id: RequestId::String(Uuid::new_v4().to_string()), - params: GetAuthStatusParams { - include_token: Some(true), - refresh_token: Some(false), - }, + params: None, }) .await .ok()?; - let auth = config - .auth_config() - .load_auth(/*enable_codex_api_key_env*/ false) - .await - .ok() - .flatten()?; - if !matches!(auth, CodexAuth::Chatgpt(_)) || auth.is_fedramp_account() { - return None; - } - let saved_token = auth.get_token().ok()?; - if status.auth_method != Some(AuthMode::Chatgpt) - || status.auth_token.as_deref() != Some(saved_token.as_str()) - { - return None; - } - let identity = Identity::from_auth(&auth)?; - let client = RouteAwareClientPool::new_without_redirects( - config.http_client_factory(), - ClientRouteClass::Api, - ); - let response = client - .get(format!( - "{}/wham/security-setup", - config.chatgpt_base_url.trim_end_matches('/') - )) - .headers( - codex_model_provider::auth_provider_from_auth(&auth) - .to_auth_headers() - .ok()?, - ) - .header( - "User-Agent", - codex_login::default_client::get_codex_user_agent(), - ) - .send() - .await - .ok()?; - if !response.status().is_success() { - return None; - } - let notice = response.json::().await.ok()?.notice?; + let fetched = response.notice?; + let identity = Identity { + account: fetched.account_id, + user: fetched.user_id, + }; + let notice = Notice { + title: fetched.title, + description: fetched.description, + action: Action { + label: fetched.action.label, + url: fetched.action.url, + }, + }; tracing::debug!(valid = notice.valid(), "received security setup notice"); if !notice.valid() { return None; diff --git a/sdk/python/src/openai_codex/generated/v2_all.py b/sdk/python/src/openai_codex/generated/v2_all.py index 5a839ab215..4335881c6c 100644 --- a/sdk/python/src/openai_codex/generated/v2_all.py +++ b/sdk/python/src/openai_codex/generated/v2_all.py @@ -52,6 +52,25 @@ class AccountRoutingOverride(Enum): us_cr = "us_cr" +class AccountSecuritySetupAction(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + label: str + url: str + + +class AccountSecuritySetupNotice(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + account_id: Annotated[str, Field(alias="accountId")] + action: AccountSecuritySetupAction + description: str + title: str + user_id: Annotated[str, Field(alias="userId")] + + class AccountSessionWorkspaceKind(Enum): personal = "personal" workspace = "workspace" @@ -1919,6 +1938,13 @@ class GetAccountRateLimitsParams(BaseModel): ] = None +class GetAccountSecuritySetupResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + notice: AccountSecuritySetupNotice | None = None + + class GetAccountTokenUsageParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -8148,6 +8174,18 @@ class AccountLogoutRequest(BaseModel): params: Annotated[None, Field(None)] = None +class AccountSecuritySetupReadRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["account/securitySetup/read"], + Field(title="Account/securitySetup/readRequestMethod"), + ] + params: Annotated[None, Field(None)] = None + + class AccountRateLimitsReadRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -13699,6 +13737,7 @@ class ClientRequest( | AccountLoginStartRequest | AccountLoginCancelRequest | AccountLogoutRequest + | AccountSecuritySetupReadRequest | AccountRateLimitsReadRequest | AccountRateLimitResetCreditConsumeRequest | AccountUsageReadRequest @@ -13810,6 +13849,7 @@ class ClientRequest( | AccountLoginStartRequest | AccountLoginCancelRequest | AccountLogoutRequest + | AccountSecuritySetupReadRequest | AccountRateLimitsReadRequest | AccountRateLimitResetCreditConsumeRequest | AccountUsageReadRequest