diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index a790874de0..71b346cc46 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -956,40 +956,6 @@ ], "title": "ChatgptLoginAccountParams", "type": "object" - }, - { - "description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE. The access token must contain the same scopes that Codex-managed ChatGPT auth tokens have.", - "properties": { - "accessToken": { - "description": "Access token (JWT) supplied by the client. This token is used for backend API requests and email extraction.", - "type": "string" - }, - "chatgptAccountId": { - "description": "Workspace/account identifier supplied by the client.", - "type": "string" - }, - "chatgptPlanType": { - "description": "Optional plan type supplied by the client.\n\nWhen `null`, Codex attempts to derive the plan type from access-token claims. If unavailable, the plan defaults to `unknown`.", - "type": [ - "string", - "null" - ] - }, - "type": { - "enum": [ - "chatgptAuthTokens" - ], - "title": "ChatgptAuthTokensLoginAccountParamsType", - "type": "string" - } - }, - "required": [ - "accessToken", - "chatgptAccountId", - "type" - ], - "title": "ChatgptAuthTokensLoginAccountParams", - "type": "object" } ] }, diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index fcb205db04..167962b0f3 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -3901,106 +3901,6 @@ "title": "FuzzyFileSearch/sessionCompletedNotification", "type": "object" }, - { - "properties": { - "method": { - "enum": [ - "thread/realtime/started" - ], - "title": "Thread/realtime/startedNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/ThreadRealtimeStartedNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/realtime/startedNotification", - "type": "object" - }, - { - "properties": { - "method": { - "enum": [ - "thread/realtime/itemAdded" - ], - "title": "Thread/realtime/itemAddedNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/ThreadRealtimeItemAddedNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/realtime/itemAddedNotification", - "type": "object" - }, - { - "properties": { - "method": { - "enum": [ - "thread/realtime/outputAudio/delta" - ], - "title": "Thread/realtime/outputAudio/deltaNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/ThreadRealtimeOutputAudioDeltaNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/realtime/outputAudio/deltaNotification", - "type": "object" - }, - { - "properties": { - "method": { - "enum": [ - "thread/realtime/error" - ], - "title": "Thread/realtime/errorNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/ThreadRealtimeErrorNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/realtime/errorNotification", - "type": "object" - }, - { - "properties": { - "method": { - "enum": [ - "thread/realtime/closed" - ], - "title": "Thread/realtime/closedNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/ThreadRealtimeClosedNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/realtime/closedNotification", - "type": "object" - }, { "description": "Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.", "properties": { 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 c26cea19a4..48af60022a 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 @@ -7538,106 +7538,6 @@ "title": "FuzzyFileSearch/sessionCompletedNotification", "type": "object" }, - { - "properties": { - "method": { - "enum": [ - "thread/realtime/started" - ], - "title": "Thread/realtime/startedNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/v2/ThreadRealtimeStartedNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/realtime/startedNotification", - "type": "object" - }, - { - "properties": { - "method": { - "enum": [ - "thread/realtime/itemAdded" - ], - "title": "Thread/realtime/itemAddedNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/v2/ThreadRealtimeItemAddedNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/realtime/itemAddedNotification", - "type": "object" - }, - { - "properties": { - "method": { - "enum": [ - "thread/realtime/outputAudio/delta" - ], - "title": "Thread/realtime/outputAudio/deltaNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/v2/ThreadRealtimeOutputAudioDeltaNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/realtime/outputAudio/deltaNotification", - "type": "object" - }, - { - "properties": { - "method": { - "enum": [ - "thread/realtime/error" - ], - "title": "Thread/realtime/errorNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/v2/ThreadRealtimeErrorNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/realtime/errorNotification", - "type": "object" - }, - { - "properties": { - "method": { - "enum": [ - "thread/realtime/closed" - ], - "title": "Thread/realtime/closedNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/v2/ThreadRealtimeClosedNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/realtime/closedNotification", - "type": "object" - }, { "description": "Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.", "properties": { @@ -11362,40 +11262,6 @@ ], "title": "Chatgptv2::LoginAccountParams", "type": "object" - }, - { - "description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE. The access token must contain the same scopes that Codex-managed ChatGPT auth tokens have.", - "properties": { - "accessToken": { - "description": "Access token (JWT) supplied by the client. This token is used for backend API requests and email extraction.", - "type": "string" - }, - "chatgptAccountId": { - "description": "Workspace/account identifier supplied by the client.", - "type": "string" - }, - "chatgptPlanType": { - "description": "Optional plan type supplied by the client.\n\nWhen `null`, Codex attempts to derive the plan type from access-token claims. If unavailable, the plan defaults to `unknown`.", - "type": [ - "string", - "null" - ] - }, - "type": { - "enum": [ - "chatgptAuthTokens" - ], - "title": "ChatgptAuthTokensv2::LoginAccountParamsType", - "type": "string" - } - }, - "required": [ - "accessToken", - "chatgptAccountId", - "type" - ], - "title": "ChatgptAuthTokensv2::LoginAccountParams", - "type": "object" } ], "title": "LoginAccountParams" 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 bbf05bfed0..32fc5ef1e0 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 @@ -7613,40 +7613,6 @@ ], "title": "Chatgptv2::LoginAccountParams", "type": "object" - }, - { - "description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE. The access token must contain the same scopes that Codex-managed ChatGPT auth tokens have.", - "properties": { - "accessToken": { - "description": "Access token (JWT) supplied by the client. This token is used for backend API requests and email extraction.", - "type": "string" - }, - "chatgptAccountId": { - "description": "Workspace/account identifier supplied by the client.", - "type": "string" - }, - "chatgptPlanType": { - "description": "Optional plan type supplied by the client.\n\nWhen `null`, Codex attempts to derive the plan type from access-token claims. If unavailable, the plan defaults to `unknown`.", - "type": [ - "string", - "null" - ] - }, - "type": { - "enum": [ - "chatgptAuthTokens" - ], - "title": "ChatgptAuthTokensv2::LoginAccountParamsType", - "type": "string" - } - }, - "required": [ - "accessToken", - "chatgptAccountId", - "type" - ], - "title": "ChatgptAuthTokensv2::LoginAccountParams", - "type": "object" } ], "title": "LoginAccountParams" @@ -11505,106 +11471,6 @@ "title": "FuzzyFileSearch/sessionCompletedNotification", "type": "object" }, - { - "properties": { - "method": { - "enum": [ - "thread/realtime/started" - ], - "title": "Thread/realtime/startedNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/ThreadRealtimeStartedNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/realtime/startedNotification", - "type": "object" - }, - { - "properties": { - "method": { - "enum": [ - "thread/realtime/itemAdded" - ], - "title": "Thread/realtime/itemAddedNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/ThreadRealtimeItemAddedNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/realtime/itemAddedNotification", - "type": "object" - }, - { - "properties": { - "method": { - "enum": [ - "thread/realtime/outputAudio/delta" - ], - "title": "Thread/realtime/outputAudio/deltaNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/ThreadRealtimeOutputAudioDeltaNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/realtime/outputAudio/deltaNotification", - "type": "object" - }, - { - "properties": { - "method": { - "enum": [ - "thread/realtime/error" - ], - "title": "Thread/realtime/errorNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/ThreadRealtimeErrorNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/realtime/errorNotification", - "type": "object" - }, - { - "properties": { - "method": { - "enum": [ - "thread/realtime/closed" - ], - "title": "Thread/realtime/closedNotificationMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/ThreadRealtimeClosedNotification" - } - }, - "required": [ - "method", - "params" - ], - "title": "Thread/realtime/closedNotification", - "type": "object" - }, { "description": "Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/LoginAccountParams.json b/codex-rs/app-server-protocol/schema/json/v2/LoginAccountParams.json index ce6bdd4a3f..5e6bc2f27e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/LoginAccountParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/LoginAccountParams.json @@ -36,40 +36,6 @@ ], "title": "Chatgptv2::LoginAccountParams", "type": "object" - }, - { - "description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE. The access token must contain the same scopes that Codex-managed ChatGPT auth tokens have.", - "properties": { - "accessToken": { - "description": "Access token (JWT) supplied by the client. This token is used for backend API requests and email extraction.", - "type": "string" - }, - "chatgptAccountId": { - "description": "Workspace/account identifier supplied by the client.", - "type": "string" - }, - "chatgptPlanType": { - "description": "Optional plan type supplied by the client.\n\nWhen `null`, Codex attempts to derive the plan type from access-token claims. If unavailable, the plan defaults to `unknown`.", - "type": [ - "string", - "null" - ] - }, - "type": { - "enum": [ - "chatgptAuthTokens" - ], - "title": "ChatgptAuthTokensv2::LoginAccountParamsType", - "type": "string" - } - }, - "required": [ - "accessToken", - "chatgptAccountId", - "type" - ], - "title": "ChatgptAuthTokensv2::LoginAccountParams", - "type": "object" } ], "title": "LoginAccountParams" diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts index daf23faa2c..4cf4cc76f0 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts @@ -31,11 +31,6 @@ import type { TerminalInteractionNotification } from "./v2/TerminalInteractionNo import type { ThreadArchivedNotification } from "./v2/ThreadArchivedNotification"; import type { ThreadClosedNotification } from "./v2/ThreadClosedNotification"; import type { ThreadNameUpdatedNotification } from "./v2/ThreadNameUpdatedNotification"; -import type { ThreadRealtimeClosedNotification } from "./v2/ThreadRealtimeClosedNotification"; -import type { ThreadRealtimeErrorNotification } from "./v2/ThreadRealtimeErrorNotification"; -import type { ThreadRealtimeItemAddedNotification } from "./v2/ThreadRealtimeItemAddedNotification"; -import type { ThreadRealtimeOutputAudioDeltaNotification } from "./v2/ThreadRealtimeOutputAudioDeltaNotification"; -import type { ThreadRealtimeStartedNotification } from "./v2/ThreadRealtimeStartedNotification"; import type { ThreadStartedNotification } from "./v2/ThreadStartedNotification"; import type { ThreadStatusChangedNotification } from "./v2/ThreadStatusChangedNotification"; import type { ThreadTokenUsageUpdatedNotification } from "./v2/ThreadTokenUsageUpdatedNotification"; @@ -50,4 +45,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/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "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": "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/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "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/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "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": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "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/LoginAccountParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/LoginAccountParams.ts index ef668f9c1a..fefdeb1ec7 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/LoginAccountParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/LoginAccountParams.ts @@ -2,20 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type LoginAccountParams = { "type": "apiKey", apiKey: string, } | { "type": "chatgpt" } | { "type": "chatgptAuthTokens", -/** - * Access token (JWT) supplied by the client. - * This token is used for backend API requests and email extraction. - */ -accessToken: string, -/** - * Workspace/account identifier supplied by the client. - */ -chatgptAccountId: string, -/** - * Optional plan type supplied by the client. - * - * When `null`, Codex attempts to derive the plan type from access-token - * claims. If unavailable, the plan defaults to `unknown`. - */ -chatgptPlanType?: string | null, }; +export type LoginAccountParams ={ "type": "apiKey", apiKey: string, } | { "type": "chatgpt" }; diff --git a/codex-rs/app-server-protocol/src/experimental_api.rs b/codex-rs/app-server-protocol/src/experimental_api.rs index 05f45600d9..8bede0274e 100644 --- a/codex-rs/app-server-protocol/src/experimental_api.rs +++ b/codex-rs/app-server-protocol/src/experimental_api.rs @@ -23,6 +23,36 @@ pub fn experimental_fields() -> Vec<&'static ExperimentalField> { inventory::iter::.into_iter().collect() } +/// Describes how an experimental enum variant appears on the wire. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExperimentalEnumVariantEncoding { + /// A plain string-literal union arm, such as `"chatgptAuthTokens"`. + StringLiteral, + /// A tagged object arm, such as `{ "type": "chatgptAuthTokens", ... }`. + TaggedObject { tag_name: &'static str }, + /// An externally tagged object arm, such as `{ "reject": { ... } }`. + ExternallyTaggedObject, +} + +/// Describes an experimental enum variant on a specific type. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ExperimentalEnumVariant { + pub type_name: &'static str, + pub serialized_name: &'static str, + pub reason: &'static str, + pub encoding: ExperimentalEnumVariantEncoding, +} + +inventory::collect!(ExperimentalEnumVariant); + +/// Returns all experimental enum variants registered across the protocol +/// types. +pub fn experimental_enum_variants() -> Vec<&'static ExperimentalEnumVariant> { + inventory::iter:: + .into_iter() + .collect() +} + /// Constructs a consistent error message for experimental gating. pub fn experimental_required_message(reason: &str) -> String { format!("{reason} requires experimentalApi capability") @@ -31,8 +61,11 @@ pub fn experimental_required_message(reason: &str) -> String { #[cfg(test)] mod tests { use super::ExperimentalApi as ExperimentalApiTrait; + use super::ExperimentalEnumVariantEncoding; + use super::experimental_enum_variants; use codex_experimental_api_macros::ExperimentalApi; use pretty_assertions::assert_eq; + use serde::Serialize; #[allow(dead_code)] #[derive(ExperimentalApi)] @@ -67,4 +100,31 @@ mod tests { None ); } + + #[allow(dead_code)] + #[derive(ExperimentalApi, Serialize)] + #[serde(tag = "type", rename_all = "camelCase")] + enum TaggedEnumVariantShapes { + Stable, + #[experimental("tagged/experimental")] + ExperimentalVariant, + } + + #[test] + fn derive_registers_experimental_enum_variants_with_wire_shape_metadata() { + let variant = experimental_enum_variants() + .into_iter() + .find(|variant| variant.reason == "tagged/experimental") + .expect("tagged experimental variant should be registered"); + + assert_eq!( + *variant, + crate::experimental_api::ExperimentalEnumVariant { + type_name: "TaggedEnumVariantShapes", + serialized_name: "experimentalVariant", + reason: "tagged/experimental", + encoding: ExperimentalEnumVariantEncoding::TaggedObject { tag_name: "type" }, + } + ); + } } diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index ba151046a1..5e63401bd1 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -2,6 +2,9 @@ use crate::ClientNotification; use crate::ClientRequest; use crate::ServerNotification; use crate::ServerRequest; +use crate::experimental_api::ExperimentalEnumVariant; +use crate::experimental_api::ExperimentalEnumVariantEncoding; +use crate::experimental_api::experimental_enum_variants; use crate::experimental_api::experimental_fields; use crate::export_client_notification_schemas; use crate::export_client_param_schemas; @@ -220,6 +223,7 @@ pub fn generate_json_with_experimental(out_dir: &Path, experimental_api: bool) - fn filter_experimental_ts(out_dir: &Path) -> Result<()> { let registered_fields = experimental_fields(); + let registered_enum_variants = experimental_enum_variants(); let experimental_method_types = experimental_method_types(); // Most generated TS files are filtered by schema processing, but // `ClientRequest.ts` and any type with `#[experimental(...)]` fields need @@ -227,6 +231,7 @@ fn filter_experimental_ts(out_dir: &Path) -> Result<()> { // file-local unions/interfaces. filter_client_request_ts(out_dir, EXPERIMENTAL_CLIENT_METHODS)?; filter_experimental_type_fields_ts(out_dir, ®istered_fields)?; + filter_experimental_enum_variants_ts(out_dir, ®istered_enum_variants)?; remove_generated_type_files(out_dir, &experimental_method_types, "ts")?; Ok(()) } @@ -327,15 +332,163 @@ fn filter_experimental_fields_in_ts_file( Ok(()) } +fn filter_experimental_enum_variants_ts( + out_dir: &Path, + experimental_variants: &[&'static ExperimentalEnumVariant], +) -> Result<()> { + let mut variants_by_type_name: HashMap> = + HashMap::new(); + for variant in experimental_variants { + variants_by_type_name + .entry(variant.type_name.to_string()) + .or_default() + .push(*variant); + } + + for path in ts_files_in_recursive(out_dir)? { + let Some(type_name) = path.file_stem().and_then(|stem| stem.to_str()) else { + continue; + }; + let Some(type_variants) = variants_by_type_name.get(type_name) else { + continue; + }; + filter_experimental_enum_variants_in_ts_file(&path, type_variants)?; + } + Ok(()) +} + +fn filter_experimental_enum_variants_in_ts_file( + path: &Path, + experimental_variants: &[&ExperimentalEnumVariant], +) -> Result<()> { + let mut content = + fs::read_to_string(path).with_context(|| format!("Failed to read {}", path.display()))?; + let Some((prefix, body, suffix)) = split_type_alias(&content) else { + return Ok(()); + }; + + let filtered_arms: Vec = split_top_level(&body, '|') + .into_iter() + .filter(|arm| { + !experimental_variants + .iter() + .any(|variant| ts_union_arm_matches_experimental_enum_variant(arm, variant)) + }) + .collect(); + let new_body = filtered_arms.join(" | "); + content = format!("{prefix}{new_body}{suffix}"); + let import_usage_scope = split_type_alias(&content) + .map(|(_, body, _)| body) + .unwrap_or_else(|| new_body.clone()); + content = prune_unused_type_imports(content, &import_usage_scope); + fs::write(path, content).with_context(|| format!("Failed to write {}", path.display()))?; + Ok(()) +} + fn filter_experimental_schema(bundle: &mut Value) -> Result<()> { let registered_fields = experimental_fields(); + let registered_enum_variants = experimental_enum_variants(); filter_experimental_fields_in_root(bundle, ®istered_fields); filter_experimental_fields_in_definitions(bundle, ®istered_fields); + filter_experimental_enum_variants_in_root(bundle, ®istered_enum_variants); + filter_experimental_enum_variants_in_definitions(bundle, ®istered_enum_variants); prune_experimental_methods(bundle, EXPERIMENTAL_CLIENT_METHODS); remove_experimental_method_type_definitions(bundle); Ok(()) } +fn filter_experimental_enum_variants_in_root( + schema: &mut Value, + experimental_variants: &[&'static ExperimentalEnumVariant], +) { + let Some(title) = schema.get("title").and_then(Value::as_str) else { + return; + }; + let title = title.to_string(); + + let matching_variants: Vec<&ExperimentalEnumVariant> = experimental_variants + .iter() + .copied() + .filter(|variant| title == variant.type_name) + .collect(); + if matching_variants.is_empty() { + return; + } + remove_experimental_enum_variants_from_schema(schema, &matching_variants); +} + +fn filter_experimental_enum_variants_in_definitions( + bundle: &mut Value, + experimental_variants: &[&'static ExperimentalEnumVariant], +) { + let Some(definitions) = bundle.get_mut("definitions").and_then(Value::as_object_mut) else { + return; + }; + + filter_experimental_enum_variants_in_definitions_map(definitions, experimental_variants); +} + +fn filter_experimental_enum_variants_in_definitions_map( + definitions: &mut Map, + experimental_variants: &[&'static ExperimentalEnumVariant], +) { + for (def_name, def_schema) in definitions.iter_mut() { + if is_namespace_map(def_schema) { + if let Some(namespace_defs) = def_schema.as_object_mut() { + filter_experimental_enum_variants_in_definitions_map( + namespace_defs, + experimental_variants, + ); + } + continue; + } + + let matching_variants: Vec<&ExperimentalEnumVariant> = experimental_variants + .iter() + .copied() + .filter(|variant| definition_matches_type(def_name, variant.type_name)) + .collect(); + if matching_variants.is_empty() { + continue; + } + remove_experimental_enum_variants_from_schema(def_schema, &matching_variants); + } +} + +fn remove_experimental_enum_variants_from_schema( + schema: &mut Value, + experimental_variants: &[&ExperimentalEnumVariant], +) { + match schema { + Value::Array(items) => { + items.retain(|item| { + !experimental_variants + .iter() + .any(|variant| json_schema_matches_experimental_enum_variant(item, variant)) + }); + for item in items { + remove_experimental_enum_variants_from_schema(item, experimental_variants); + } + } + Value::Object(map) => { + if let Some(enum_values) = map.get_mut("enum").and_then(Value::as_array_mut) { + enum_values.retain(|value| { + !experimental_variants.iter().any(|variant| { + matches!( + variant.encoding, + ExperimentalEnumVariantEncoding::StringLiteral + ) && value.as_str() == Some(variant.serialized_name) + }) + }); + } + for child in map.values_mut() { + remove_experimental_enum_variants_from_schema(child, experimental_variants); + } + } + Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {} + } +} + fn filter_experimental_fields_in_root( schema: &mut Value, experimental_fields: &[&'static crate::experimental_api::ExperimentalField], @@ -715,6 +868,105 @@ fn parse_property(input: &str) -> Option<(String, &str)> { Some((name, input[colon_index + 1..].trim_start())) } +fn ts_union_arm_matches_experimental_enum_variant( + arm: &str, + variant: &ExperimentalEnumVariant, +) -> bool { + let arm = strip_leading_block_comments(arm).trim(); + match variant.encoding { + ExperimentalEnumVariantEncoding::StringLiteral => { + arm == format!("\"{}\"", variant.serialized_name) + } + ExperimentalEnumVariantEncoding::TaggedObject { tag_name } => { + ts_object_arm_has_string_property(arm, tag_name, variant.serialized_name) + } + ExperimentalEnumVariantEncoding::ExternallyTaggedObject => { + ts_object_arm_has_property(arm, variant.serialized_name) + } + } +} + +fn ts_object_arm_has_string_property(arm: &str, property_name: &str, property_value: &str) -> bool { + let Some((open, close)) = find_top_level_brace_span(arm) else { + return false; + }; + let inner = &arm[open + 1..close]; + for field in split_top_level(inner, ',') { + let field = strip_leading_block_comments(field.as_str()); + let Some((name, value)) = parse_property(field) else { + continue; + }; + if name != property_name { + continue; + } + return parse_string_literal(strip_leading_block_comments(value).trim_start()) + .is_some_and(|(value, _)| value == property_value); + } + false +} + +fn ts_object_arm_has_property(arm: &str, property_name: &str) -> bool { + let Some((open, close)) = find_top_level_brace_span(arm) else { + return false; + }; + let inner = &arm[open + 1..close]; + split_top_level(inner, ',').into_iter().any(|field| { + let field = strip_leading_block_comments(field.as_str()); + parse_property_name(field).is_some_and(|name| name == property_name) + }) +} + +fn json_schema_matches_experimental_enum_variant( + schema: &Value, + variant: &ExperimentalEnumVariant, +) -> bool { + match variant.encoding { + ExperimentalEnumVariantEncoding::StringLiteral => { + json_schema_matches_string_literal(schema, variant.serialized_name) + } + ExperimentalEnumVariantEncoding::TaggedObject { tag_name } => { + json_schema_has_tagged_variant(schema, tag_name, variant.serialized_name) + } + ExperimentalEnumVariantEncoding::ExternallyTaggedObject => { + json_schema_has_property(schema, variant.serialized_name) + } + } +} + +fn json_schema_matches_string_literal(schema: &Value, literal: &str) -> bool { + let Value::Object(map) = schema else { + return false; + }; + if map.get("const").and_then(Value::as_str) == Some(literal) { + return true; + } + map.get("enum") + .and_then(Value::as_array) + .is_some_and(|values| values.len() == 1 && values[0].as_str() == Some(literal)) +} + +fn json_schema_has_tagged_variant(schema: &Value, tag_name: &str, tag_value: &str) -> bool { + let Value::Object(map) = schema else { + return false; + }; + let Some(properties) = map.get("properties").and_then(Value::as_object) else { + return false; + }; + properties + .get(tag_name) + .is_some_and(|tag_schema| json_schema_matches_string_literal(tag_schema, tag_value)) +} + +fn json_schema_has_property(schema: &Value, property_name: &str) -> bool { + let Value::Object(map) = schema else { + return false; + }; + let Some(properties) = map.get("properties").and_then(Value::as_object) else { + return false; + }; + properties.contains_key(property_name) +} + fn strip_leading_block_comments(input: &str) -> &str { let mut rest = input.trim_start(); loop { @@ -1942,6 +2194,12 @@ mod tests { let thread_start_ts = fs::read_to_string(output_dir.join("v2").join("ThreadStartParams.ts"))?; assert_eq!(thread_start_ts.contains("mockExperimentalField"), false); + let login_account_params_ts = + fs::read_to_string(output_dir.join("v2").join("LoginAccountParams.ts"))?; + assert_eq!( + login_account_params_ts.contains("\"chatgptAuthTokens\""), + false + ); assert_eq!( output_dir .join("v2") @@ -2197,6 +2455,12 @@ mod tests { let thread_start_ts = fs::read_to_string(output_dir.join("v2").join("ThreadStartParams.ts"))?; assert_eq!(thread_start_ts.contains("mockExperimentalField"), true); + let login_account_params_ts = + fs::read_to_string(output_dir.join("v2").join("LoginAccountParams.ts"))?; + assert_eq!( + login_account_params_ts.contains("\"chatgptAuthTokens\""), + true + ); let command_execution_request_approval_ts = fs::read_to_string( output_dir .join("v2") @@ -2662,6 +2926,12 @@ export type Config = { stableField: Keep, unstableField: string | null } & ({ [k let thread_start_json = fs::read_to_string(output_dir.join("v2").join("ThreadStartParams.json"))?; assert_eq!(thread_start_json.contains("mockExperimentalField"), false); + let login_account_params_json = + fs::read_to_string(output_dir.join("v2").join("LoginAccountParams.json"))?; + assert_eq!( + login_account_params_json.contains("chatgptAuthTokens"), + false + ); let command_execution_request_approval_json = fs::read_to_string(output_dir.join("CommandExecutionRequestApprovalParams.json"))?; assert_eq!( diff --git a/codex-rs/codex-experimental-api-macros/src/lib.rs b/codex-rs/codex-experimental-api-macros/src/lib.rs index 6262be3869..4b918ee831 100644 --- a/codex-rs/codex-experimental-api-macros/src/lib.rs +++ b/codex-rs/codex-experimental-api-macros/src/lib.rs @@ -13,6 +13,13 @@ use syn::LitStr; use syn::Type; use syn::parse_macro_input; +#[derive(Default)] +struct EnumSerdeConfig { + rename_all: Option, + tag: Option, + untagged: bool, +} + #[proc_macro_derive(ExperimentalApi, attributes(experimental))] pub fn derive_experimental_api(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); @@ -141,7 +148,10 @@ fn derive_for_struct(input: &DeriveInput, data: &DataStruct) -> TokenStream { fn derive_for_enum(input: &DeriveInput, data: &DataEnum) -> TokenStream { let name = &input.ident; + let type_name_lit = LitStr::new(&name.to_string(), Span::call_site()); + let serde_config = enum_serde_config(&input.attrs); let mut match_arms = Vec::new(); + let mut registrations = Vec::new(); for variant in &data.variants { let variant_name = &variant.ident; @@ -155,6 +165,14 @@ fn derive_for_enum(input: &DeriveInput, data: &DataEnum) -> TokenStream { match_arms.push(quote! { #pattern => Some(#reason), }); + if let Some(registration) = experimental_enum_variant_registration( + &type_name_lit, + &serde_config, + variant, + &reason, + ) { + registrations.push(registration); + } } else { match_arms.push(quote! { #pattern => None, @@ -163,6 +181,8 @@ fn derive_for_enum(input: &DeriveInput, data: &DataEnum) -> TokenStream { } let expanded = quote! { + #(#registrations)* + impl crate::experimental_api::ExperimentalApi for #name { fn experimental_reason(&self) -> Option<&'static str> { match self { @@ -181,12 +201,160 @@ fn experimental_reason(attrs: &[Attribute]) -> Option { attr.parse_args::().ok() } +fn enum_serde_config(attrs: &[Attribute]) -> EnumSerdeConfig { + let mut config = EnumSerdeConfig::default(); + for attr in attrs.iter().filter(|attr| attr.path().is_ident("serde")) { + let _ = attr.parse_nested_meta(|meta| { + if meta.path.is_ident("rename_all") { + config.rename_all = Some(meta.value()?.parse::()?.value()); + } else if meta.path.is_ident("tag") { + config.tag = Some(meta.value()?.parse::()?.value()); + } else if meta.path.is_ident("untagged") { + config.untagged = true; + } + Ok(()) + }); + } + config +} + +fn variant_serialized_name(variant: &syn::Variant, rename_all: Option<&str>) -> String { + if let Some(rename) = serde_rename(&variant.attrs) { + return rename; + } + apply_rename_all(&variant.ident.to_string(), rename_all) +} + +fn serde_rename(attrs: &[Attribute]) -> Option { + let mut rename = None; + for attr in attrs.iter().filter(|attr| attr.path().is_ident("serde")) { + let _ = attr.parse_nested_meta(|meta| { + if meta.path.is_ident("rename") { + rename = Some(meta.value()?.parse::()?.value()); + } + Ok(()) + }); + } + rename +} + +fn experimental_enum_variant_registration( + type_name_lit: &LitStr, + serde_config: &EnumSerdeConfig, + variant: &syn::Variant, + reason: &LitStr, +) -> Option { + if serde_config.untagged { + return None; + } + + let serialized_name = variant_serialized_name(variant, serde_config.rename_all.as_deref()); + let serialized_name_lit = LitStr::new(&serialized_name, Span::call_site()); + let encoding = if let Some(tag_name) = serde_config.tag.as_deref() { + let tag_name_lit = LitStr::new(tag_name, Span::call_site()); + quote! { + crate::experimental_api::ExperimentalEnumVariantEncoding::TaggedObject { + tag_name: #tag_name_lit, + } + } + } else if matches!(variant.fields, Fields::Unit) { + quote! { + crate::experimental_api::ExperimentalEnumVariantEncoding::StringLiteral + } + } else { + quote! { + crate::experimental_api::ExperimentalEnumVariantEncoding::ExternallyTaggedObject + } + }; + + Some(quote! { + ::inventory::submit! { + crate::experimental_api::ExperimentalEnumVariant { + type_name: #type_name_lit, + serialized_name: #serialized_name_lit, + reason: #reason, + encoding: #encoding, + } + } + }) +} + fn field_serialized_name(field: &Field) -> Option { let ident = field.ident.as_ref()?; let name = ident.to_string(); Some(snake_to_camel(&name)) } +fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String { + let words = split_words(name); + match rename_all { + Some("camelCase") => { + let mut out = String::new(); + for (index, word) in words.iter().enumerate() { + if index == 0 { + out.push_str(word); + } else { + out.push_str(&capitalize(word)); + } + } + out + } + Some("snake_case") => words.join("_"), + Some("kebab-case") => words.join("-"), + Some("PascalCase") => words + .iter() + .map(|word| capitalize(word)) + .collect::>() + .join(""), + Some("SCREAMING_SNAKE_CASE") => words.join("_").to_ascii_uppercase(), + Some("UPPERCASE") => words.concat().to_ascii_uppercase(), + Some("lowercase") => words.concat(), + Some(_) | None => name.to_string(), + } +} + +fn split_words(name: &str) -> Vec { + let mut words = Vec::new(); + let mut current = String::new(); + let chars: Vec = name.chars().collect(); + for (index, ch) in chars.iter().copied().enumerate() { + if ch == '_' || ch == '-' { + if !current.is_empty() { + words.push(std::mem::take(&mut current)); + } + continue; + } + + let previous = chars.get(index.wrapping_sub(1)).copied(); + let next = chars.get(index + 1).copied(); + let boundary_before = previous.is_some_and(|previous| { + (previous.is_ascii_lowercase() && ch.is_ascii_uppercase()) + || (previous.is_ascii_uppercase() + && ch.is_ascii_uppercase() + && next.is_some_and(|next| next.is_ascii_lowercase())) + }); + if boundary_before && !current.is_empty() { + words.push(std::mem::take(&mut current)); + } + current.push(ch.to_ascii_lowercase()); + } + if !current.is_empty() { + words.push(current); + } + words +} + +fn capitalize(word: &str) -> String { + let mut chars = word.chars(); + let Some(first) = chars.next() else { + return String::new(); + }; + let mut out = String::new(); + out.push(first.to_ascii_uppercase()); + out.extend(chars); + out +} + fn snake_to_camel(s: &str) -> String { let mut out = String::with_capacity(s.len()); let mut upper = false;