diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 7334b25a94..ed50dfc392 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -583,6 +583,29 @@ } ] }, + "AuthRecoveryNotification": { + "properties": { + "message": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "message", + "provider", + "threadId", + "turnId" + ], + "type": "object" + }, "AutoReviewDecisionSource": { "description": "[UNSTABLE] Source that produced a terminal approval auto-review decision.", "enum": [ @@ -7779,6 +7802,46 @@ "title": "Model/verificationNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "modelProvider/authRecoveryStarted" + ], + "title": "ModelProvider/authRecoveryStartedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AuthRecoveryNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "ModelProvider/authRecoveryStartedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "modelProvider/authRecoveryCompleted" + ], + "title": "ModelProvider/authRecoveryCompletedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AuthRecoveryNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "ModelProvider/authRecoveryCompletedNotification", + "type": "object" + }, { "properties": { "method": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 5bcf81b5c8..44579f4e72 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 @@ -5572,6 +5572,46 @@ "title": "Model/verificationNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "modelProvider/authRecoveryStarted" + ], + "title": "ModelProvider/authRecoveryStartedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/AuthRecoveryNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "ModelProvider/authRecoveryStartedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "modelProvider/authRecoveryCompleted" + ], + "title": "ModelProvider/authRecoveryCompletedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/AuthRecoveryNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "ModelProvider/authRecoveryCompletedNotification", + "type": "object" + }, { "properties": { "method": { @@ -7669,6 +7709,31 @@ } ] }, + "AuthRecoveryNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "message": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "message", + "provider", + "threadId", + "turnId" + ], + "title": "AuthRecoveryNotification", + "type": "object" + }, "AutoCompactTokenLimitScope": { "description": "Selects which part of the active context is charged against `model_auto_compact_token_limit`.", "oneOf": [ 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 14696965b9..6de28207e1 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 @@ -1245,6 +1245,31 @@ } ] }, + "AuthRecoveryNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "message": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "message", + "provider", + "threadId", + "turnId" + ], + "title": "AuthRecoveryNotification", + "type": "object" + }, "AutoCompactTokenLimitScope": { "description": "Selects which part of the active context is charged against `model_auto_compact_token_limit`.", "oneOf": [ @@ -16430,6 +16455,46 @@ "title": "Model/verificationNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "modelProvider/authRecoveryStarted" + ], + "title": "ModelProvider/authRecoveryStartedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AuthRecoveryNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "ModelProvider/authRecoveryStartedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "modelProvider/authRecoveryCompleted" + ], + "title": "ModelProvider/authRecoveryCompletedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AuthRecoveryNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "ModelProvider/authRecoveryCompletedNotification", + "type": "object" + }, { "properties": { "method": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/AuthRecoveryNotification.json b/codex-rs/app-server-protocol/schema/json/v2/AuthRecoveryNotification.json new file mode 100644 index 0000000000..c27e4a8a09 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/AuthRecoveryNotification.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "message": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "message", + "provider", + "threadId", + "turnId" + ], + "title": "AuthRecoveryNotification", + "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 43b016a047..2ac72f3632 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 1cf22af4a1..c10c4d6c61 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/ServerNotification.ts b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts index 1ee34f8cae..c65fccb789 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts @@ -8,6 +8,7 @@ import type { AccountRateLimitsUpdatedNotification } from "./v2/AccountRateLimit import type { AccountUpdatedNotification } from "./v2/AccountUpdatedNotification"; import type { AgentMessageDeltaNotification } from "./v2/AgentMessageDeltaNotification"; import type { AppListUpdatedNotification } from "./v2/AppListUpdatedNotification"; +import type { AuthRecoveryNotification } from "./v2/AuthRecoveryNotification"; import type { CommandExecOutputDeltaNotification } from "./v2/CommandExecOutputDeltaNotification"; import type { CommandExecutionOutputDeltaNotification } from "./v2/CommandExecutionOutputDeltaNotification"; import type { ConfigWarningNotification } from "./v2/ConfigWarningNotification"; @@ -85,4 +86,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW /** * Notification sent from the server to the client. */ -export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/deleted", "params": ThreadDeletedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "thread/reverted", "params": ThreadRevertedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/goal/updated", "params": ThreadGoalUpdatedNotification } | { "method": "thread/goal/cleared", "params": ThreadGoalClearedNotification } | { "method": "thread/queue/changed", "params": ThreadQueueChangedNotification } | { "method": "project/changed", "params": ProjectChangedNotification } | { "method": "thread/project/updated", "params": ThreadProjectUpdatedNotification } | { "method": "thread/environment/connected", "params": EnvironmentConnectionNotification } | { "method": "thread/environment/disconnected", "params": EnvironmentConnectionNotification } | { "method": "thread/settings/updated", "params": ThreadSettingsUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "autoApprovalReview/strictReviewRequired", "params": StrictReviewRequiredNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "rawResponse/completed", "params": RawResponseCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "process/outputDelta", "params": ProcessOutputDeltaNotification } | { "method": "process/exited", "params": ProcessExitedNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "mcpServer/event/stream/notification", "params": McpServerEventStreamNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "remoteControl/status/changed", "params": RemoteControlStatusChangedNotification } | { "method": "externalAgentConfig/import/progress", "params": ExternalAgentConfigImportProgressNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "model/verification", "params": ModelVerificationNotification } | { "method": "turn/moderationMetadata", "params": TurnModerationMetadataNotification } | { "method": "model/safetyBuffering/updated", "params": ModelSafetyBufferingUpdatedNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/item/started", "params": ThreadRealtimeItemStartedNotification } | { "method": "thread/realtime/item/transcript/delta", "params": ThreadRealtimeItemTranscriptDeltaNotification } | { "method": "thread/realtime/item/completed", "params": ThreadRealtimeItemCompletedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }; +export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/deleted", "params": ThreadDeletedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "thread/reverted", "params": ThreadRevertedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/goal/updated", "params": ThreadGoalUpdatedNotification } | { "method": "thread/goal/cleared", "params": ThreadGoalClearedNotification } | { "method": "thread/queue/changed", "params": ThreadQueueChangedNotification } | { "method": "project/changed", "params": ProjectChangedNotification } | { "method": "thread/project/updated", "params": ThreadProjectUpdatedNotification } | { "method": "thread/environment/connected", "params": EnvironmentConnectionNotification } | { "method": "thread/environment/disconnected", "params": EnvironmentConnectionNotification } | { "method": "thread/settings/updated", "params": ThreadSettingsUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "autoApprovalReview/strictReviewRequired", "params": StrictReviewRequiredNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "rawResponse/completed", "params": RawResponseCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "process/outputDelta", "params": ProcessOutputDeltaNotification } | { "method": "process/exited", "params": ProcessExitedNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "mcpServer/event/stream/notification", "params": McpServerEventStreamNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "remoteControl/status/changed", "params": RemoteControlStatusChangedNotification } | { "method": "externalAgentConfig/import/progress", "params": ExternalAgentConfigImportProgressNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "model/verification", "params": ModelVerificationNotification } | { "method": "modelProvider/authRecoveryStarted", "params": AuthRecoveryNotification } | { "method": "modelProvider/authRecoveryCompleted", "params": AuthRecoveryNotification } | { "method": "turn/moderationMetadata", "params": TurnModerationMetadataNotification } | { "method": "model/safetyBuffering/updated", "params": ModelSafetyBufferingUpdatedNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/item/started", "params": ThreadRealtimeItemStartedNotification } | { "method": "thread/realtime/item/transcript/delta", "params": ThreadRealtimeItemTranscriptDeltaNotification } | { "method": "thread/realtime/item/completed", "params": ThreadRealtimeItemCompletedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerNotificationEnvelope.ts b/codex-rs/app-server-protocol/schema/typescript/ServerNotificationEnvelope.ts index f6aa0d7d87..bb5799a140 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerNotificationEnvelope.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerNotificationEnvelope.ts @@ -8,6 +8,7 @@ import type { AccountRateLimitsUpdatedNotification } from "./v2/AccountRateLimit import type { AccountUpdatedNotification } from "./v2/AccountUpdatedNotification"; import type { AgentMessageDeltaNotification } from "./v2/AgentMessageDeltaNotification"; import type { AppListUpdatedNotification } from "./v2/AppListUpdatedNotification"; +import type { AuthRecoveryNotification } from "./v2/AuthRecoveryNotification"; import type { CommandExecOutputDeltaNotification } from "./v2/CommandExecOutputDeltaNotification"; import type { CommandExecutionOutputDeltaNotification } from "./v2/CommandExecutionOutputDeltaNotification"; import type { ConfigWarningNotification } from "./v2/ConfigWarningNotification"; @@ -95,4 +96,4 @@ export type ServerNotificationEnvelope = { * Optional so clients can decode notifications from older app-server * versions. Current app-server versions always populate it. */ -emittedAtMs?: number, } & ({ "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/deleted", "params": ThreadDeletedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "thread/reverted", "params": ThreadRevertedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/goal/updated", "params": ThreadGoalUpdatedNotification } | { "method": "thread/goal/cleared", "params": ThreadGoalClearedNotification } | { "method": "thread/queue/changed", "params": ThreadQueueChangedNotification } | { "method": "project/changed", "params": ProjectChangedNotification } | { "method": "thread/project/updated", "params": ThreadProjectUpdatedNotification } | { "method": "thread/environment/connected", "params": EnvironmentConnectionNotification } | { "method": "thread/environment/disconnected", "params": EnvironmentConnectionNotification } | { "method": "thread/settings/updated", "params": ThreadSettingsUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "autoApprovalReview/strictReviewRequired", "params": StrictReviewRequiredNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "rawResponse/completed", "params": RawResponseCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "process/outputDelta", "params": ProcessOutputDeltaNotification } | { "method": "process/exited", "params": ProcessExitedNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "mcpServer/event/stream/notification", "params": McpServerEventStreamNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "remoteControl/status/changed", "params": RemoteControlStatusChangedNotification } | { "method": "externalAgentConfig/import/progress", "params": ExternalAgentConfigImportProgressNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "model/verification", "params": ModelVerificationNotification } | { "method": "turn/moderationMetadata", "params": TurnModerationMetadataNotification } | { "method": "model/safetyBuffering/updated", "params": ModelSafetyBufferingUpdatedNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/item/started", "params": ThreadRealtimeItemStartedNotification } | { "method": "thread/realtime/item/transcript/delta", "params": ThreadRealtimeItemTranscriptDeltaNotification } | { "method": "thread/realtime/item/completed", "params": ThreadRealtimeItemCompletedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }); +emittedAtMs?: number, } & ({ "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/deleted", "params": ThreadDeletedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "thread/reverted", "params": ThreadRevertedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/goal/updated", "params": ThreadGoalUpdatedNotification } | { "method": "thread/goal/cleared", "params": ThreadGoalClearedNotification } | { "method": "thread/queue/changed", "params": ThreadQueueChangedNotification } | { "method": "project/changed", "params": ProjectChangedNotification } | { "method": "thread/project/updated", "params": ThreadProjectUpdatedNotification } | { "method": "thread/environment/connected", "params": EnvironmentConnectionNotification } | { "method": "thread/environment/disconnected", "params": EnvironmentConnectionNotification } | { "method": "thread/settings/updated", "params": ThreadSettingsUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "autoApprovalReview/strictReviewRequired", "params": StrictReviewRequiredNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "rawResponse/completed", "params": RawResponseCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "process/outputDelta", "params": ProcessOutputDeltaNotification } | { "method": "process/exited", "params": ProcessExitedNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "mcpServer/event/stream/notification", "params": McpServerEventStreamNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "remoteControl/status/changed", "params": RemoteControlStatusChangedNotification } | { "method": "externalAgentConfig/import/progress", "params": ExternalAgentConfigImportProgressNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "model/verification", "params": ModelVerificationNotification } | { "method": "modelProvider/authRecoveryStarted", "params": AuthRecoveryNotification } | { "method": "modelProvider/authRecoveryCompleted", "params": AuthRecoveryNotification } | { "method": "turn/moderationMetadata", "params": TurnModerationMetadataNotification } | { "method": "model/safetyBuffering/updated", "params": ModelSafetyBufferingUpdatedNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/item/started", "params": ThreadRealtimeItemStartedNotification } | { "method": "thread/realtime/item/transcript/delta", "params": ThreadRealtimeItemTranscriptDeltaNotification } | { "method": "thread/realtime/item/completed", "params": ThreadRealtimeItemCompletedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }); diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AuthRecoveryNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AuthRecoveryNotification.ts new file mode 100644 index 0000000000..62d8d06368 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AuthRecoveryNotification.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 AuthRecoveryNotification = { threadId: string, turnId: string, provider: string, message: string, }; 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 ff809bd52d..282c4d3bae 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -42,6 +42,7 @@ export type { AppsReadResponse } from "./AppsReadResponse"; export type { AskForApproval } from "./AskForApproval"; export type { AttestationGenerateParams } from "./AttestationGenerateParams"; export type { AttestationGenerateResponse } from "./AttestationGenerateResponse"; +export type { AuthRecoveryNotification } from "./AuthRecoveryNotification"; export type { AutoReviewDecisionSource } from "./AutoReviewDecisionSource"; export type { AutoReviewRequirements } from "./AutoReviewRequirements"; export type { BrowserUseAccessApprovalLifetime } from "./BrowserUseAccessApprovalLifetime"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 156d16001b..f9735b4afd 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1912,6 +1912,8 @@ server_notification_definitions! { ContextCompacted => "thread/compacted" (v2::ContextCompactedNotification), ModelRerouted => "model/rerouted" (v2::ModelReroutedNotification), ModelVerification => "model/verification" (v2::ModelVerificationNotification), + AuthRecoveryStarted => "modelProvider/authRecoveryStarted" (v2::AuthRecoveryNotification), + AuthRecoveryCompleted => "modelProvider/authRecoveryCompleted" (v2::AuthRecoveryNotification), #[experimental("turn/moderationMetadata")] TurnModerationMetadata => "turn/moderationMetadata" (v2::TurnModerationMetadataNotification), ModelSafetyBufferingUpdated => "model/safetyBuffering/updated" (v2::ModelSafetyBufferingUpdatedNotification), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/notification.rs b/codex-rs/app-server-protocol/src/protocol/v2/notification.rs index 8cca97ccd1..ca0e7753f3 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/notification.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/notification.rs @@ -5,6 +5,16 @@ use crate::TS; use serde::Deserialize; use serde::Serialize; +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AuthRecoveryNotification { + pub thread_id: String, + pub turn_id: String, + pub provider: String, + pub message: String, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 57cb16fa27..bd0ce53b88 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1749,6 +1749,8 @@ The app-server streams JSON-RPC notifications while a turn is running. Each turn - `model/safetyBuffering/updated` — `{ threadId, turnId, model, useCases, reasons, showBufferingUi, fasterModel }` when a response enters safety buffering. `fasterModel` is nullable. This notification is transient and is not persisted in rollout history. - `model/rerouted` — `{ threadId, turnId, fromModel, toModel, reason }` when the backend reroutes a request to a different model (for example, due to high-risk cyber safety checks). - `model/verification` — `{ threadId, turnId, verifications }` when the backend flags additional account verification, such as `trustedAccessForCyber`. +- `modelProvider/authRecoveryStarted` — `{ threadId, turnId, provider, message }` when model-provider authentication recovery begins. +- `modelProvider/authRecoveryCompleted` — `{ threadId, turnId, provider, message }` when model-provider authentication recovery succeeds. - `turn/moderationMetadata` — experimental; `{ threadId, turnId, metadata }` when a first-party backend supplies turn-scoped moderation metadata for client-side presentation. `turn/started` carries no items. `turn/completed` carries only the final agent message as a summary fallback; continue consuming `item/*` notifications for the full canonical item list. diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 11c06d7f2a..d06938d781 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -13,6 +13,7 @@ use crate::thread_status::ThreadWatchActiveGuard; use crate::thread_status::ThreadWatchManager; use codex_app_server_protocol::AccountRateLimitsUpdatedNotification; use codex_app_server_protocol::AdditionalPermissionProfile as V2AdditionalPermissionProfile; +use codex_app_server_protocol::AuthRecoveryNotification; use codex_app_server_protocol::CodexErrorInfo as V2CodexErrorInfo; use codex_app_server_protocol::CommandAction as V2ParsedCommand; use codex_app_server_protocol::CommandExecutionApprovalDecision; @@ -252,6 +253,30 @@ pub(crate) async fn apply_bespoke_event_handling( )) .await; } + EventMsg::AuthRecoveryStarted(event) => { + outgoing + .send_server_notification(ServerNotification::AuthRecoveryStarted( + AuthRecoveryNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id, + provider: event.provider, + message: event.message, + }, + )) + .await; + } + EventMsg::AuthRecoveryCompleted(event) => { + outgoing + .send_server_notification(ServerNotification::AuthRecoveryCompleted( + AuthRecoveryNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id, + provider: event.provider, + message: event.message, + }, + )) + .await; + } EventMsg::Warning(warning_event) => { let notification = WarningNotification { thread_id: Some(conversation_id.to_string()), @@ -2180,6 +2205,7 @@ mod tests { use codex_protocol::plan_tool::StepStatus; use codex_protocol::protocol::AgentMessageEvent; use codex_protocol::protocol::AskForApproval; + use codex_protocol::protocol::AuthRecoveryEvent; use codex_protocol::protocol::CreditsSnapshot; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::GuardianAssessmentEvent; @@ -3418,11 +3444,11 @@ mod tests { }), }, conversation_id, - conversation, - thread_manager, - outgoing, - thread_state, - thread_watch_manager, + Arc::clone(&conversation), + Arc::clone(&thread_manager), + outgoing.clone(), + Arc::clone(&thread_state), + thread_watch_manager.clone(), Arc::new(tokio::sync::Semaphore::new(/*permits*/ 1)), "test-provider".to_string(), ) @@ -3437,6 +3463,52 @@ mod tests { } other => bail!("unexpected message: {other:?}"), } + + for (phase, method, event) in [ + ( + "started", + "modelProvider/authRecoveryStarted", + EventMsg::AuthRecoveryStarted as fn(AuthRecoveryEvent) -> EventMsg, + ), + ( + "completed", + "modelProvider/authRecoveryCompleted", + EventMsg::AuthRecoveryCompleted, + ), + ] { + let message = format!("Authentication recovery {phase}."); + apply_bespoke_event_handling( + Event { + id: "turn-1".to_string(), + msg: event(AuthRecoveryEvent { + provider: "test-provider".to_string(), + message: message.clone(), + }), + }, + conversation_id, + Arc::clone(&conversation), + Arc::clone(&thread_manager), + outgoing.clone(), + Arc::clone(&thread_state), + thread_watch_manager.clone(), + Arc::new(tokio::sync::Semaphore::new(/*permits*/ 1)), + "test-provider".to_string(), + ) + .await; + + assert_eq!( + serde_json::to_value(recv_broadcast_notification(&mut rx).await?)?, + json!({ + "method": method, + "params": { + "threadId": conversation_id.to_string(), + "turnId": "turn-1", + "provider": "test-provider", + "message": message, + } + }) + ); + } Ok(()) } diff --git a/codex-rs/app-server/src/transport_tests.rs b/codex-rs/app-server/src/transport_tests.rs index a44e66e2cf..5b98f9bbc8 100644 --- a/codex-rs/app-server/src/transport_tests.rs +++ b/codex-rs/app-server/src/transport_tests.rs @@ -1,4 +1,5 @@ use super::*; +use codex_app_server_protocol::AuthRecoveryNotification; use codex_app_server_protocol::ConfigWarningNotification; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerNotification; @@ -191,6 +192,43 @@ async fn experimental_notifications_are_dropped_without_capability() { writer_rx.try_recv().is_err(), "experimental notifications should not reach clients without capability" ); + + let recovery = AuthRecoveryNotification { + thread_id: "thread-1".to_string(), + turn_id: "turn-1".to_string(), + provider: "Amazon Bedrock".to_string(), + message: "Refreshing AWS authentication.".to_string(), + }; + + for (method, notification) in [ + ( + "modelProvider/authRecoveryStarted", + ServerNotification::AuthRecoveryStarted(recovery.clone()), + ), + ( + "modelProvider/authRecoveryCompleted", + ServerNotification::AuthRecoveryCompleted(recovery), + ), + ] { + route_outgoing_envelope( + &mut connections, + OutgoingEnvelope::ToConnection { + connection_id, + message: app_server_notification(notification), + write_complete_tx: None, + }, + ) + .await; + + let message = writer_rx + .recv() + .await + .expect("stable auth recovery notification should reach clients without capability"); + let OutgoingMessage::AppServerNotification(envelope) = message.message else { + panic!("stable auth recovery notification should be delivered"); + }; + assert_eq!(envelope.notification.to_string(), method); + } } #[tokio::test] diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index eec874f08c..9f03c939e0 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -30,6 +30,7 @@ use std::sync::OnceLock; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; +use async_channel::Sender; use codex_api::AgentIdentityTelemetry; use codex_api::ApiError; use codex_api::AuthProvider; @@ -83,6 +84,9 @@ use codex_protocol::config_types::Verbosity as VerbosityConfig; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; +use codex_protocol::protocol::AuthRecoveryEvent; +use codex_protocol::protocol::Event as ProtocolEvent; +use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::InternalSessionSource; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::W3cTraceContext; @@ -291,6 +295,7 @@ pub struct ModelClient { agent_identity_policy: AgentIdentityAuthPolicy, prompt_cache_key_override: Option, free_guardian_enabled: bool, + event_sender: Option>, http_client_factory: HttpClientFactory, } @@ -512,6 +517,7 @@ impl ModelClient { agent_identity_policy, prompt_cache_key_override: None, free_guardian_enabled: false, + event_sender: None, http_client_factory, } } @@ -521,11 +527,13 @@ impl ModelClient { self } - pub(crate) fn with_prompt_cache_key_override( + pub(crate) fn with_session_context( mut self, prompt_cache_key_override: Option, + event_sender: Sender, ) -> Self { self.prompt_cache_key_override = prompt_cache_key_override; + self.event_sender = Some(event_sender); self } @@ -1672,6 +1680,8 @@ impl ModelClientSession { &mut provider_auth_recovery_attempted, session_telemetry, &self.client.state.provider, + self.client.event_sender.as_ref(), + responses_metadata.turn_id.as_deref(), ) .await?, ); @@ -1805,6 +1815,8 @@ impl ModelClientSession { &mut provider_auth_recovery_attempted, session_telemetry, &provider, + self.client.event_sender.as_ref(), + responses_metadata.turn_id.as_deref(), ) .await?, ); @@ -2380,18 +2392,57 @@ struct WebsocketConnectParams<'a> { endpoint: ResponsesEndpoint, } +fn emit_auth_recovery_event( + event_sender: Option<&Sender>, + turn_id: Option<&str>, + provider: &SharedModelProvider, + message: &str, + event: fn(AuthRecoveryEvent) -> EventMsg, +) { + if let (Some(sender), Some(turn_id)) = (event_sender, turn_id) { + let _ = sender.try_send(ProtocolEvent { + id: turn_id.to_string(), + msg: event(AuthRecoveryEvent { + provider: provider.info().name.clone(), + message: message.to_string(), + }), + }); + } +} + async fn handle_unauthorized( transport: TransportError, auth_recovery: &mut Option, provider_auth_recovery_attempted: &mut bool, session_telemetry: &SessionTelemetry, provider: &SharedModelProvider, + event_sender: Option<&Sender>, + turn_id: Option<&str>, ) -> Result { let debug = extract_response_debug_context(&transport); if !*provider_auth_recovery_attempted { *provider_auth_recovery_attempted = true; + let messages = provider.auth_recovery_messages(); + if let Some(messages) = messages { + emit_auth_recovery_event( + event_sender, + turn_id, + provider, + messages.started, + EventMsg::AuthRecoveryStarted, + ); + } match provider.recover_from_unauthorized().await { Ok(ProviderUnauthorizedRecovery::Recovered) => { + if let Some(messages) = messages { + emit_auth_recovery_event( + event_sender, + turn_id, + provider, + messages.succeeded, + EventMsg::AuthRecoveryCompleted, + ); + } return Ok(UnauthorizedRecoveryExecution { mode: "provider", phase: "provider_refresh", diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index 75fc1251f8..6eda1c86bf 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -31,6 +31,7 @@ use codex_model_provider::BearerAuthProvider; use codex_model_provider::ModelProvider; use codex_model_provider::ModelProviderFuture; use codex_model_provider::ProviderAccountResult; +use codex_model_provider::ProviderAuthRecoveryMessages; use codex_model_provider::ProviderUnauthorizedRecovery; use codex_model_provider::SharedModelProvider; use codex_model_provider::create_model_provider; @@ -895,6 +896,8 @@ async fn bedrock_unauthorized_error_uses_provider_mapping() { &mut provider_auth_recovery_attempted, &test_session_telemetry(), &provider, + /*event_sender*/ None, + /*turn_id*/ None, ) .await .expect_err("expired Bedrock signature should fail"); @@ -931,6 +934,13 @@ impl ModelProvider for TestRecoveryProvider { self.inner.account_state() } + fn auth_recovery_messages(&self) -> Option { + Some(ProviderAuthRecoveryMessages { + started: "Refreshing provider authentication.", + succeeded: "Provider authentication recovered.", + }) + } + fn recover_from_unauthorized( &self, ) -> ModelProviderFuture<'_, codex_protocol::error::Result> { @@ -975,12 +985,15 @@ async fn provider_owned_auth_recovery_is_bounded_and_preserves_unauthorized_fail let mut auth_recovery = None; let mut provider_auth_recovery_attempted = false; let telemetry = test_session_telemetry(); + let (event_sender, event_receiver) = async_channel::unbounded(); let result = super::handle_unauthorized( unauthorized(), &mut auth_recovery, &mut provider_auth_recovery_attempted, &telemetry, &provider, + Some(&event_sender), + Some("turn-1"), ) .await; @@ -998,6 +1011,8 @@ async fn provider_owned_auth_recovery_is_bounded_and_preserves_unauthorized_fail &mut provider_auth_recovery_attempted, &telemetry, &provider, + Some(&event_sender), + Some("turn-1"), ) .await .expect_err("provider recovery should not run more than once") @@ -1011,6 +1026,29 @@ async fn provider_owned_auth_recovery_is_bounded_and_preserves_unauthorized_fail other => panic!("unexpected error after provider recovery: {other}"), } assert_eq!(attempts.load(Ordering::Relaxed), 1); + + let events = std::iter::from_fn(|| event_receiver.try_recv().ok()) + .map(|event| serde_json::to_value(event).expect("recovery event should serialize")) + .collect::>(); + let mut expected = vec![json!({ + "id": "turn-1", + "msg": { + "type": "auth_recovery_started", + "provider": provider.info().name, + "message": "Refreshing provider authentication.", + } + })]; + if !should_fail { + expected.push(json!({ + "id": "turn-1", + "msg": { + "type": "auth_recovery_completed", + "provider": provider.info().name, + "message": "Provider authentication recovered.", + } + })); + } + assert_eq!(events, expected); } } diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index c5082fda91..6da32bc1aa 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1422,11 +1422,12 @@ impl Session { config.http_client_factory(), ) .with_free_guardian_enabled(config.free_guardian_enabled()) - .with_prompt_cache_key_override( + .with_session_context( crate::guardian::prompt_cache_key_override_for_review_session( &session_configuration.session_source, session_configuration.parent_thread_id, ), + tx_event.clone(), ), executed_tool_calls: executed_tool_calls.clone(), code_mode_service: crate::tools::code_mode::CodeModeService::new( diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 4a749b28b8..1583f2d9b0 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -1797,6 +1797,8 @@ pub(super) fn realtime_text_for_event(msg: &EventMsg) -> Option<(String, Option< } EventMsg::Error(_) | EventMsg::Warning(_) + | EventMsg::AuthRecoveryStarted(_) + | EventMsg::AuthRecoveryCompleted(_) | EventMsg::GuardianWarning(_) | EventMsg::RealtimeConversationStarted(_) | EventMsg::RealtimeConversationSdp(_) diff --git a/codex-rs/core/tests/suite/external_auth.rs b/codex-rs/core/tests/suite/external_auth.rs index 5d2e01434f..f5afbe77c0 100644 --- a/codex-rs/core/tests/suite/external_auth.rs +++ b/codex-rs/core/tests/suite/external_auth.rs @@ -1,3 +1,4 @@ +use codex_core::TurnInputRequest; use codex_login::AuthHeaders; use codex_login::CodexAuth; use codex_login::ExternalAuth; @@ -9,6 +10,9 @@ use codex_model_provider_info::ModelProviderAwsAuthInfo; use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::WireApi; use codex_model_provider_info::create_oss_provider_with_base_url; +use codex_protocol::protocol::AuthRecoveryEvent; +use codex_protocol::protocol::EventMsg; +use codex_protocol::user_input::UserInput; use codex_utils_redacted_string::RedactedString; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_response_created; @@ -317,7 +321,42 @@ async fn amazon_bedrock_aws_auth_refresh_resigns() -> anyhow::Result<()> { config.model_provider = provider; }); let test = builder.build_with_auto_env(&server).await?; - test.submit_turn("hello").await?; + test.codex + .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { + text: "hello".to_string(), + text_elements: Vec::new(), + }])) + .await?; + + let mut recovery_events = Vec::new(); + loop { + match core_test_support::wait_for_event(&test.codex, |_| true).await { + EventMsg::AuthRecoveryStarted(event) => recovery_events.push(("started", event)), + EventMsg::AuthRecoveryCompleted(event) => recovery_events.push(("completed", event)), + EventMsg::TurnComplete(_) => break, + _ => {} + } + } + + assert_eq!( + recovery_events, + vec![ + ( + "started", + AuthRecoveryEvent { + provider: "Amazon Bedrock".to_string(), + message: "AWS session has expired. Reauthenticating...".to_string(), + }, + ), + ( + "completed", + AuthRecoveryEvent { + provider: "Amazon Bedrock".to_string(), + message: "Signed in with AWS.".to_string(), + }, + ), + ] + ); server.verify().await; Ok(()) } diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index e3463d7d1d..6aba2618a7 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -239,6 +239,14 @@ impl EventProcessor for EventProcessorWithHumanOutput { CodexStatus::Running } ServerNotification::Warning(notification) => self.process_warning(notification.message), + ServerNotification::AuthRecoveryStarted(notification) => { + eprintln!("{}", notification.message); + CodexStatus::Running + } + ServerNotification::AuthRecoveryCompleted(notification) => { + eprintln!("{}", notification.message.style(self.green)); + CodexStatus::Running + } ServerNotification::Error(notification) => { eprintln!( "{} {}", diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index b449a7195f..9552613cc7 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -1419,6 +1419,10 @@ fn should_process_notification( .thread_id .as_deref() .is_none_or(|candidate| candidate == thread_id), + ServerNotification::AuthRecoveryStarted(notification) + | ServerNotification::AuthRecoveryCompleted(notification) => { + notification.thread_id == thread_id && notification.turn_id == turn_id + } ServerNotification::Error(notification) => { notification.thread_id == thread_id && notification.turn_id == turn_id } diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index 050b586a5c..317baf3c0a 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -1,4 +1,5 @@ use super::*; +use codex_app_server_protocol::AuthRecoveryNotification; use codex_otel::set_parent_from_w3c_trace_context; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::models::ActivePermissionProfile; @@ -289,6 +290,28 @@ fn runtime_warnings_are_filtered_to_the_primary_thread() { }); assert_eq!(outcomes, [true, true, false]); + + let recovery = AuthRecoveryNotification { + thread_id: primary_thread_id.to_string(), + turn_id: turn_id.to_string(), + provider: "example".to_string(), + message: "Refresh authentication".to_string(), + }; + let outcomes = [ + ServerNotification::AuthRecoveryStarted(recovery.clone()), + ServerNotification::AuthRecoveryCompleted(recovery.clone()), + ServerNotification::AuthRecoveryStarted(AuthRecoveryNotification { + thread_id: "thread-2".to_string(), + ..recovery.clone() + }), + ServerNotification::AuthRecoveryCompleted(AuthRecoveryNotification { + turn_id: "turn-2".to_string(), + ..recovery + }), + ] + .map(|notification| should_process_notification(¬ification, primary_thread_id, turn_id)); + + assert_eq!(outcomes, [true, true, false, false]); } #[tokio::test] diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index e96526deb5..fdc32c3a07 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -247,6 +247,8 @@ async fn run_codex_tool_session_inner( break; } EventMsg::Warning(_) + | EventMsg::AuthRecoveryStarted(_) + | EventMsg::AuthRecoveryCompleted(_) | EventMsg::GuardianWarning(_) | EventMsg::ModelVerification(_) | EventMsg::SafetyBuffering(_) diff --git a/codex-rs/model-provider/src/amazon_bedrock/mod.rs b/codex-rs/model-provider/src/amazon_bedrock/mod.rs index 94b2421564..1543348e95 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/mod.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/mod.rs @@ -34,6 +34,7 @@ use crate::provider::ModelProvider; use crate::provider::ModelProviderFuture; use crate::provider::ProviderAccountResult; use crate::provider::ProviderAccountState; +use crate::provider::ProviderAuthRecoveryMessages; use crate::provider::ProviderCapabilities; use crate::provider::ProviderUnauthorizedRecovery; use crate::provider::RemoteCompactionSupport; @@ -244,6 +245,14 @@ impl ModelProvider for AmazonBedrockModelProvider { ) || (self.uses_aws_auth_recovery() && error::is_refreshable_auth_error(error)) } + fn auth_recovery_messages(&self) -> Option { + self.uses_aws_auth_recovery() + .then_some(ProviderAuthRecoveryMessages { + started: "AWS session has expired. Reauthenticating...", + succeeded: "Signed in with AWS.", + }) + } + fn recover_from_unauthorized( &self, ) -> ModelProviderFuture<'_, Result> { diff --git a/codex-rs/model-provider/src/lib.rs b/codex-rs/model-provider/src/lib.rs index b7d72a6f17..98ed5f5e7b 100644 --- a/codex-rs/model-provider/src/lib.rs +++ b/codex-rs/model-provider/src/lib.rs @@ -23,6 +23,7 @@ pub use provider::ModelProviderFuture; pub use provider::ProviderAccountError; pub use provider::ProviderAccountResult; pub use provider::ProviderAccountState; +pub use provider::ProviderAuthRecoveryMessages; pub use provider::ProviderCapabilities; pub use provider::ProviderUnauthorizedRecovery; pub use provider::RemoteCompactionSupport; diff --git a/codex-rs/model-provider/src/provider.rs b/codex-rs/model-provider/src/provider.rs index 6aa6941a3c..198cd63731 100644 --- a/codex-rs/model-provider/src/provider.rs +++ b/codex-rs/model-provider/src/provider.rs @@ -92,6 +92,13 @@ pub enum ProviderUnauthorizedRecovery { Recovered, } +/// User-facing lifecycle messages for provider-owned authentication recovery. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProviderAuthRecoveryMessages { + pub started: &'static str, + pub succeeded: &'static str, +} + /// Error returned when a provider cannot construct its app-visible account state. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ProviderAccountError { @@ -192,6 +199,11 @@ pub trait ModelProvider: fmt::Debug + Send + Sync { ) } + /// Returns lifecycle messages when provider-owned authentication recovery is active. + fn auth_recovery_messages(&self) -> Option { + None + } + /// Attempts provider-owned authentication recovery before using the auth manager. fn recover_from_unauthorized( &self, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index f5986aa3ae..09de97f8c8 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1340,6 +1340,12 @@ pub enum EventMsg { /// indicates the turn continued but the user should still be notified. Warning(WarningEvent), + /// Provider-owned authentication recovery has started for the current turn. + AuthRecoveryStarted(AuthRecoveryEvent), + + /// Provider-owned authentication recovery has completed for the current turn. + AuthRecoveryCompleted(AuthRecoveryEvent), + /// Warning issued by the guardian automatic approval reviewer. GuardianWarning(WarningEvent), @@ -2053,6 +2059,15 @@ pub struct WarningEvent { pub message: String, } +/// User-facing progress for provider-owned authentication recovery. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] +pub struct AuthRecoveryEvent { + /// Display name of the model provider whose authentication is recovering. + pub provider: String, + /// User-facing description of the authentication recovery stage. + pub message: String, +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "snake_case")] #[ts(rename_all = "snake_case")] diff --git a/codex-rs/rollout-trace/src/protocol_event.rs b/codex-rs/rollout-trace/src/protocol_event.rs index 8f0441981a..dadc4bb0ac 100644 --- a/codex-rs/rollout-trace/src/protocol_event.rs +++ b/codex-rs/rollout-trace/src/protocol_event.rs @@ -363,6 +363,8 @@ pub(crate) fn tool_runtime_trace_event(event: &EventMsg) -> Option None, EventMsg::Error(_) | EventMsg::Warning(_) + | EventMsg::AuthRecoveryStarted(_) + | EventMsg::AuthRecoveryCompleted(_) | EventMsg::GuardianWarning(_) | EventMsg::SafetyBuffering(_) | EventMsg::RealtimeConversationStarted(_) @@ -442,7 +444,9 @@ pub(crate) fn wrapped_protocol_event_type(event: &EventMsg) -> Option<&'static s EventMsg::Error(_) => Some("error"), EventMsg::Warning(_) => Some("warning"), EventMsg::ShutdownComplete => Some("shutdown_complete"), - EventMsg::GuardianWarning(_) + EventMsg::AuthRecoveryStarted(_) + | EventMsg::AuthRecoveryCompleted(_) + | EventMsg::GuardianWarning(_) | EventMsg::SafetyBuffering(_) | EventMsg::RealtimeConversationStarted(_) | EventMsg::RealtimeConversationRealtime(_) diff --git a/codex-rs/rollout/src/policy.rs b/codex-rs/rollout/src/policy.rs index 418f9ac345..56be88b738 100644 --- a/codex-rs/rollout/src/policy.rs +++ b/codex-rs/rollout/src/policy.rs @@ -148,6 +148,8 @@ pub fn should_persist_event_msg(ev: &EventMsg, history_mode: ThreadHistoryMode) | EventMsg::DynamicToolCallRequest(_) | EventMsg::DynamicToolCallResponse(_) | EventMsg::Warning(_) + | EventMsg::AuthRecoveryStarted(_) + | EventMsg::AuthRecoveryCompleted(_) | EventMsg::GuardianWarning(_) | EventMsg::RealtimeConversationStarted(_) | EventMsg::RealtimeConversationSdp(_) diff --git a/codex-rs/tui/src/app/app_server_event_targets.rs b/codex-rs/tui/src/app/app_server_event_targets.rs index f347964596..27359fd5c5 100644 --- a/codex-rs/tui/src/app/app_server_event_targets.rs +++ b/codex-rs/tui/src/app/app_server_event_targets.rs @@ -175,6 +175,10 @@ pub(super) fn server_notification_thread_target( ServerNotification::ThreadRealtimeClosed(notification) => { Some(notification.thread_id.as_str()) } + ServerNotification::AuthRecoveryStarted(notification) + | ServerNotification::AuthRecoveryCompleted(notification) => { + Some(notification.thread_id.as_str()) + } ServerNotification::Warning(notification) => notification.thread_id.as_deref(), ServerNotification::GuardianWarning(notification) => Some(notification.thread_id.as_str()), ServerNotification::McpServerStatusUpdated(notification) => { diff --git a/codex-rs/tui/src/chatwidget/protocol.rs b/codex-rs/tui/src/chatwidget/protocol.rs index d487babe1b..48083eb293 100644 --- a/codex-rs/tui/src/chatwidget/protocol.rs +++ b/codex-rs/tui/src/chatwidget/protocol.rs @@ -152,6 +152,14 @@ impl ChatWidget { ServerNotification::ModelSafetyBufferingUpdated(notification) => { self.on_model_safety_buffering_updated(notification, replay_kind) } + ServerNotification::AuthRecoveryStarted(notification) => { + self.add_info_message(notification.message, /*hint*/ None) + } + ServerNotification::AuthRecoveryCompleted(notification) => { + self.add_plain_history_lines(vec![ + vec!["✓ ".green(), notification.message.into()].into(), + ]); + } ServerNotification::Warning(notification) => self.on_warning(notification.message), ServerNotification::GuardianWarning(notification) => { if !notification diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index 2fb09c6124..d9e085bc50 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -1,4 +1,5 @@ use super::*; +use codex_app_server_protocol::AuthRecoveryNotification; use codex_protocol::error::CodexErr; use codex_protocol::error::CodexErrorDetails; use pretty_assertions::assert_eq; @@ -730,6 +731,32 @@ async fn live_app_server_warning_notification_renders_message() { ), "expected warning guidance, got {rendered}" ); + + let notification = AuthRecoveryNotification { + thread_id: "thread-1".to_string(), + turn_id: "turn-1".to_string(), + provider: "example".to_string(), + message: "Session expired. Sign in again.".to_string(), + }; + let mut recovery_messages = String::new(); + for notification in [ + ServerNotification::AuthRecoveryStarted(notification.clone()), + ServerNotification::AuthRecoveryCompleted(AuthRecoveryNotification { + message: "Signed in successfully.".to_string(), + ..notification + }), + ] { + chat.handle_server_notification(notification, /*replay_kind*/ None); + + let [cell] = drain_insert_history(&mut rx) + .try_into() + .expect("expected one authentication recovery history cell"); + recovery_messages.push_str(&lines_to_single_string(&cell)); + } + insta::assert_snapshot!(recovery_messages, @r" +• Session expired. Sign in again. +✓ Signed in successfully. +"); } #[tokio::test]