diff --git a/artifacts/github/bundles/openai-codex-pr-27535.json b/artifacts/github/bundles/openai-codex-pr-27535.json new file mode 100644 index 000000000..454c3f9dc --- /dev/null +++ b/artifacts/github/bundles/openai-codex-pr-27535.json @@ -0,0 +1,147 @@ +{ + "analysis_mode": "pr_first", + "commits": [ + { + "author": "celia-oai", + "committed_at": "2026-06-11T00:56:21Z", + "message": "Add secret auth storage config", + "sha": "07d66eccd904cd15cd754c43d3f74a72d63a1980", + "url": "https://github.com/openai/codex/commit/07d66eccd904cd15cd754c43d3f74a72d63a1980" + }, + { + "author": "celia-oai", + "committed_at": "2026-06-12T18:51:10Z", + "message": "refactor", + "sha": "3e3379b56006b9e877f5db4ba65009b4ad41c49c", + "url": "https://github.com/openai/codex/commit/3e3379b56006b9e877f5db4ba65009b4ad41c49c" + }, + { + "author": "celia-oai", + "committed_at": "2026-06-11T03:51:19Z", + "message": "Add auth-specific secret namespaces", + "sha": "e026ff2133ba7c2e740eee1ecf0cc0ec5a7479a8", + "url": "https://github.com/openai/codex/commit/e026ff2133ba7c2e740eee1ecf0cc0ec5a7479a8" + }, + { + "author": "celia-oai", + "committed_at": "2026-06-12T19:40:24Z", + "message": "changes", + "sha": "30f6722a49e562b8d8fccbaf221aa278d39ab30c", + "url": "https://github.com/openai/codex/commit/30f6722a49e562b8d8fccbaf221aa278d39ab30c" + } + ], + "default_branch": "main", + "docs_refs": [], + "examples_refs": [], + "extracted_flags": [ + "CLI", + "MCP", + "CONFIG_TOML_FILE", + "LOCAL_FS", + "FEATURES", + "KEYRING_SERVICE", + "SECRETS_VERSION", + "LOCAL_SECRETS_FILENAME", + "CODEX_AUTH_SECRETS_FILENAME", + "MCP_OAUTH_SECRETS_FILENAME", + "TUI", + "UNIX_EPOCH", + "TEST_SECRET" + ], + "files": [ + { + "additions": 20, + "deletions": 0, + "patch_excerpt": "@@ -113,6 +113,26 @@ pub enum OAuthCredentialsStoreMode {\n Keyring,\n }\n \n+/// Determine how auth credentials should use keyring-backed storage.\n+#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]\n+#[serde(rename_all = \"lowercase\")]\n+pub enum AuthKeyringBackendKind {\n+ /// Store the serialized auth payload directly in the OS keyring.\n+ Direct,\n+ /// Store auth payloads in the local encrypted secrets file, with the file key in the OS keyring.\n+ Secrets,\n+}\n+\n+impl Default for AuthKeyringBackendKind {\n+ fn default() -> Self {\n+ if cfg!(windows) {\n+ Self::Secrets\n+ } else {\n+ Self::Direct\n+ }\n+ }\n+}\n+\n #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]\n #[serde(rename_all = \"kebab-case\")]\n pub enum WindowsSandboxModeToml {", + "path": "codex-rs/config/src/types.rs", + "status": "modified" + }, + { + "additions": 6, + "deletions": 0, + "patch_excerpt": "@@ -599,6 +599,9 @@\n \"search_tool\": {\n \"type\": \"boolean\"\n },\n+ \"secret_auth_storage\": {\n+ \"type\": \"boolean\"\n+ },\n \"shell_snapshot\": {\n \"type\": \"boolean\"\n },\n@@ -4751,6 +4754,9 @@\n \"search_tool\": {\n \"type\": \"boolean\"\n },\n+ \"secret_auth_storage\": {\n+ \"type\": \"boolean\"\n+ },\n \"shell_snapshot\": {\n \"type\": \"boolean\"\n },", + "path": "codex-rs/core/config.schema.json", + "status": "modified" + }, + { + "additions": 59, + "deletions": 0, + "patch_excerpt": "@@ -0,0 +1,59 @@\n+use super::Config;\n+use super::ConfigTomlLoadResult;\n+use super::ManagedFeatures;\n+use codex_config::types::AuthKeyringBackendKind;\n+use codex_features::Feature;\n+use codex_features::FeatureConfigSource;\n+use codex_features::FeatureOverrides;\n+use codex_features::Features;\n+\n+impl Config {\n+ pub fn auth_keyring_backend_kind(&self) -> AuthKeyringBackendKind {\n+ auth_keyring_backend_kind_from_secret_auth_storage(\n+ self.features.enabled(Feature::SecretAuthStorage),\n+ )\n+ }\n+}\n+\n+/// Resolve the auth keyring backend from a partially loaded bootstrap config.\n+///\n+/// This is intended for startup paths that must read auth before managed cloud\n+/// requirements can be loaded and before a full [`Config`] exists.\n+pub fn resolve_bootstrap_auth_keyring_backend_kind(\n+ bootstrap_config: &ConfigTomlLoadResult,\n+) -> std::io::Result std::io::Result<()> {\n+ let config_toml = ConfigToml {\n+ features: Some(FeaturesToml::from(BTreeMap::from([(\n+ \"secret_auth_storage\".to_string(),\n+ true,\n+ )]))),\n+ ..Default::default()\n+ };\n+ assert_eq!(\n+ resolve_bootstrap_auth_keyring_backend_kind(&config_toml_load_result(\n+ config_toml,\n+ /*feature_requireme...", + "path": "codex-rs/core/src/config/auth_keyring_tests.rs", + "status": "added" + }, + { + "additions": 2, + "deletions": 0, + "patch_excerpt": "@@ -4,8 +4,10 @@ use crate::config::edit::apply_blocking;\n use assert_matches::assert_matches;\n use codex_config::CONFIG_TOML_FILE;\n use codex_config::ConfigLayerEntry;\n+use codex_config::ConfigLayerStack;\n use codex_config::ProfileV2Name;\n use codex_config::RequirementSource;\n+use codex_config::Sourced;\n use codex_config::config_toml::AgentRoleToml;\n use codex_config::config_toml::AgentsToml;\n use codex_config::config_toml::AutoReviewToml;", + "path": "codex-rs/core/src/config/config_tests.rs", + "status": "modified" + }, + { + "additions": 29, + "deletions": 1, + "patch_excerpt": "@@ -137,6 +137,7 @@ use toml::Value as TomlValue;\n use toml_edit::DocumentMut;\n \n pub(crate) mod agent_roles;\n+mod auth_keyring;\n pub mod edit;\n mod managed_features;\n mod network_proxy_spec;\n@@ -145,6 +146,7 @@ mod permissions;\n mod resolved_permission_profile;\n #[cfg(test)]\n mod schema;\n+pub use auth_keyring::resolve_bootstrap_auth_keyring_backend_kind;\n pub use codex_config::ConfigLoadOptions;\n pub use codex_config::Constrained;\n pub use codex_config::ConstraintError;\n@@ -1647,6 +1649,29 @@ pub async fn load_config_as_toml_with_cli_and_load_options(\n cli_overrides: Vec<(String, TomlValue)>,\n options: impl Into,\n ) -> std::io::Result {\n+ load_config_toml_with_layer_stack(codex_home, cwd, cli_overrides, options)\n+ .await\n+ .map(|result| result.config_toml)\n+}\n+\n+/// Partially loaded config plus the layer stack used to derive it.\n+/...", + "path": "codex-rs/core/src/config/mod.rs", + "status": "modified" + }, + { + "additions": 8, + "deletions": 0, + "patch_excerpt": "@@ -81,6 +81,8 @@ pub enum Feature {\n ShellTool,\n /// Enable Claude-style lifecycle hooks loaded from hooks.json files.\n CodexHooks,\n+ /// Store CLI auth in the encrypted local secrets backend when keyring storage is selected.\n+ SecretAuthStorage,\n \n // Experimental\n /// Enable JavaScript code mode backed by the in-process V8 runtime.\n@@ -748,6 +750,12 @@ pub const FEATURES: &[FeatureSpec] = &[\n stage: Stage::Stable,\n default_enabled: true,\n },\n+ FeatureSpec {\n+ id: Feature::SecretAuthStorage,\n+ key: \"secret_auth_storage\",\n+ stage: Stage::Stable,\n+ default_enabled: cfg!(windows),\n+ },\n FeatureSpec {\n id: Feature::UnifiedExec,\n key: \"unified_exec\",", + "path": "codex-rs/features/src/lib.rs", + "status": "modified" + }, + { + "additions": 10, + "deletions": 0, + "patch_excerpt": "@@ -189,6 +189,16 @@ fn tool_search_is_removed_and_disabled_by_default() {\n assert_eq!(feature_for_key(\"tool_search\"), Some(Feature::ToolSearch));\n }\n \n+#[test]\n+fn secret_auth_storage_defaults_to_windows_only() {\n+ assert_eq!(Feature::SecretAuthStorage.stage(), Stage::Stable);\n+ assert_eq!(Feature::SecretAuthStorage.default_enabled(), cfg!(windows));\n+ assert_eq!(\n+ feature_for_key(\"secret_auth_storage\"),\n+ Some(Feature::SecretAuthStorage)\n+ );\n+}\n+\n #[test]\n fn browser_controls_are_stable_and_enabled_by_default() {\n assert_eq!(Feature::InAppBrowser.stage(), Stage::Stable);", + "path": "codex-rs/features/src/tests.rs", + "status": "modified" + }, + { + "additions": 1, + "deletions": 1, + "patch_excerpt": "@@ -45,7 +45,7 @@ pub trait KeyringStore: Debug + Send + Sync {\n fn delete(&self, service: &str, account: &str) -> Result;\n }\n \n-#[derive(Debug)]\n+#[derive(Debug, Clone, Copy)]\n pub struct DefaultKeyringStore;\n \n impl KeyringStore for DefaultKeyringStore {", + "path": "codex-rs/keyring-store/src/lib.rs", + "status": "modified" + }, + { + "additions": 19, + "deletions": 1, + "patch_excerpt": "@@ -17,6 +17,7 @@ mod local;\n mod sanitizer;\n \n pub use local::LocalSecretsBackend;\n+pub use local::LocalSecretsNamespace;\n pub use sanitizer::redact_secrets;\n \n const KEYRING_SERVICE: &str = \"codex\";\n@@ -122,6 +123,22 @@ impl SecretsManager {\n Self { backend }\n }\n \n+ pub fn new_with_keyring_store_and_namespace(\n+ codex_home: PathBuf,\n+ backend_kind: SecretsBackendKind,\n+ keyring_store: Arc,\n+ namespace: LocalSecretsNamespace,\n+ ) -> Self {\n+ let backend: Arc = match backend_kind {\n+ SecretsBackendKind::Local => Arc::new(LocalSecretsBackend::new_with_namespace(\n+ codex_home,\n+ keyring_store,\n+ namespace,\n+ )),\n+ };\n+ Self { backend }\n+ }\n+\n pub fn set(&self, scope: &SecretScope, name: &SecretName, value: &str) -...", + "path": "codex-rs/secrets/src/lib.rs", + "status": "modified" + }, + { + "additions": 88, + "deletions": 2, + "patch_excerpt": "@@ -35,6 +35,20 @@ use super::keyring_service;\n \n const SECRETS_VERSION: u8 = 1;\n const LOCAL_SECRETS_FILENAME: &str = \"local.age\";\n+const CODEX_AUTH_SECRETS_FILENAME: &str = \"codex_auth.age\";\n+const MCP_OAUTH_SECRETS_FILENAME: &str = \"mcp_oauth.age\";\n+\n+/// Selects the local encrypted file used by a `LocalSecretsBackend`.\n+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]\n+pub enum LocalSecretsNamespace {\n+ /// General managed secrets stored in `local.age`.\n+ #[default]\n+ ManagedSecrets,\n+ /// Codex authentication credentials used by the CLI, TUI, app server, and other clients.\n+ CodexAuth,\n+ /// OAuth credentials for external MCP servers.\n+ McpOAuth,\n+}\n \n #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]\n struct SecretsFile {\n@@ -55,13 +69,27 @@ impl SecretsFile {\n pub struct LocalSecretsBackend {\n codex_home: PathBuf,\n keyring_st...", + "path": "codex-rs/secrets/src/local.rs", + "status": "modified" + } + ], + "linked_issues": [ + "#27504" + ], + "notes": [ + "Built from GitHub pull-request, commits, files, and repo endpoints." + ], + "primary_pr": { + "body": "## Why\n\nCLI auth and MCP OAuth credentials should use separate encrypted files while sharing the existing local-secrets implementation and OS-keyring-backed encryption key mechanism.\n\nThis is the second PR in the encrypted-auth stack:\n\n1. #27504 — feature and config selection\n2. This PR — auth-specific local-secrets namespaces\n3. CLI auth implementation and activation\n4. MCP OAuth implementation and activation\n\n## What Changed\n\n- Added `LocalSecretsNamespace` variants for shared secrets, CLI auth, and MCP OAuth.\n- Selected `local.age`, `cli_auth.age`, or `mcp_oauth.age` from the namespace.\n- Made atomic temporary filenames derive from the selected secrets filename.\n- Added namespaced `SecretsManager` construction and coverage proving the auth namespaces write separate encrypted files.\n- Made the default keyring store clonable for downstream namespaced auth backends.\n\nThis PR does not activate either auth backend or change existing credential behavior.\n\n## Validation\n\n- `just test -p codex-secrets` — 7 passed\n- `just test -p codex-keyring-store` — package has no test binaries\n- `just fmt`\n", + "labels": [], + "merged_at": "2026-06-12T19:52:50Z", + "number": 27535, + "state": "merged", + "title": "feat: add auth-specific encrypted secret namespaces", + "url": "https://github.com/openai/codex/pull/27535" + }, + "repo": "openai/codex", + "schema": "github_change_bundle/v1" +} diff --git a/artifacts/github/bundles/openai-codex-pr-27539.json b/artifacts/github/bundles/openai-codex-pr-27539.json new file mode 100644 index 000000000..1a4692258 --- /dev/null +++ b/artifacts/github/bundles/openai-codex-pr-27539.json @@ -0,0 +1,335 @@ +{ + "analysis_mode": "pr_first", + "commits": [ + { + "author": "celia-oai", + "committed_at": "2026-06-11T04:13:11Z", + "message": "Use encrypted local secrets for CLI auth", + "sha": "24a0cd4c2b9a14f672691335af172ce3462aeb79", + "url": "https://github.com/openai/codex/commit/24a0cd4c2b9a14f672691335af172ce3462aeb79" + }, + { + "author": "celia-oai", + "committed_at": "2026-06-12T20:34:22Z", + "message": "merge conflict", + "sha": "77343b8909e8a8ef9c6b65476c547f8681eb1554", + "url": "https://github.com/openai/codex/commit/77343b8909e8a8ef9c6b65476c547f8681eb1554" + }, + { + "author": "celia-oai", + "committed_at": "2026-06-12T20:58:06Z", + "message": "changes", + "sha": "d43079bc45170d8d8abf051718a446a592728416", + "url": "https://github.com/openai/codex/commit/d43079bc45170d8d8abf051718a446a592728416" + }, + { + "author": "celia-oai", + "committed_at": "2026-06-12T21:12:45Z", + "message": "Fix device login auth cleanup backend", + "sha": "0dc3b430c878be6d9d5a7f19e72dd6567adc6933", + "url": "https://github.com/openai/codex/commit/0dc3b430c878be6d9d5a7f19e72dd6567adc6933" + } + ], + "default_branch": "main", + "docs_refs": [], + "examples_refs": [], + "extracted_flags": [ + "CLI", + "MCP", + "TUI", + "CLIENT_ID", + "REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR", + "API", + "LOGIN_SUCCESS_MESSAGE", + "CHATGPT_LOGIN_DISABLED_MESSAGE", + "OSS", + "WORKSPACE_ID_ALLOWED", + "CODEX_HOME", + "CODEX_AUTH_SECRET_NAME", + "CODEX_AUTH", + "KEYRING_SERVICE", + "URL_SAFE_NO_PAD", + "WORKSPACE_ID_SECOND_ALLOWED", + "CODEX_ACCESS_TOKEN_ENV_VAR", + "REFRESH_TOKEN" + ], + "files": [ + { + "additions": 1, + "deletions": 0, + "patch_excerpt": "@@ -3221,6 +3221,7 @@ dependencies = [\n \"codex-model-provider-info\",\n \"codex-otel\",\n \"codex-protocol\",\n+ \"codex-secrets\",\n \"codex-terminal-detection\",\n \"codex-utils-template\",\n \"core_test_support\",", + "path": "codex-rs/Cargo.lock", + "status": "modified" + }, + { + "additions": 8, + "deletions": 0, + "patch_excerpt": "@@ -30,6 +30,7 @@ use codex_config::types::AuthCredentialsStoreMode;\n use codex_core::test_support::auth_manager_from_auth;\n use codex_core::test_support::auth_manager_from_auth_with_home;\n use codex_login::AuthDotJson;\n+use codex_login::AuthKeyringBackendKind;\n use codex_login::AuthManager;\n use codex_login::CodexAuth;\n use codex_login::save_auth;\n@@ -933,6 +934,7 @@ async fn remote_control_start_allows_missing_auth_when_enabled() {\n /*enable_codex_api_key_env*/ false,\n AuthCredentialsStoreMode::File,\n /*chatgpt_base_url*/ None,\n+ AuthKeyringBackendKind::default(),\n )\n .await;\n let (transport_event_tx, _transport_event_rx) =\n@@ -1744,6 +1746,7 @@ async fn remote_control_waits_for_account_id_before_enrolling() {\n codex_home.path(),\n &remote_control_auth_dot_json(/*account_id*/ None),\n AuthCredentialsStoreMode::File,\n+ ...", + "path": "codex-rs/app-server-transport/src/transport/remote_control/tests.rs", + "status": "modified" + }, + { + "additions": 7, + "deletions": 0, + "patch_excerpt": "@@ -7,6 +7,7 @@ use codex_app_server_protocol::RemoteControlClientsListParams;\n use codex_app_server_protocol::RemoteControlClientsListResponse;\n use codex_app_server_protocol::RemoteControlClientsRevokeParams;\n use codex_app_server_protocol::RemoteControlClientsRevokeResponse;\n+use codex_login::AuthKeyringBackendKind;\n use pretty_assertions::assert_eq;\n \n fn client_management_handle(\n@@ -173,13 +174,15 @@ async fn list_remote_control_clients_recovers_auth_after_unauthorized() {\n codex_home.path(),\n &stale_auth,\n AuthCredentialsStoreMode::File,\n+ AuthKeyringBackendKind::default(),\n )\n .expect(\"stale auth should save\");\n let auth_manager = AuthManager::shared(\n codex_home.path().to_path_buf(),\n /*enable_codex_api_key_env*/ false,\n AuthCredentialsStoreMode::File,\n /*chatgpt_base_url*/ None,\n+ AuthKeyringBacke...", + "path": "codex-rs/app-server-transport/src/transport/remote_control/tests/clients_tests.rs", + "status": "modified" + }, + { + "additions": 7, + "deletions": 0, + "patch_excerpt": "@@ -1,6 +1,7 @@\n use super::super::protocol::RemoteControlPairingStatusRequest;\n use super::super::protocol::StartRemoteControlPairingRequest;\n use super::*;\n+use codex_login::AuthKeyringBackendKind;\n use pretty_assertions::assert_eq;\n use std::io;\n \n@@ -528,13 +529,15 @@ async fn remote_control_handle_recovers_auth_before_refreshing_pairing() {\n codex_home.path(),\n &stale_auth,\n AuthCredentialsStoreMode::File,\n+ AuthKeyringBackendKind::default(),\n )\n .expect(\"stale auth should save\");\n let auth_manager = AuthManager::shared(\n codex_home.path().to_path_buf(),\n /*enable_codex_api_key_env*/ false,\n AuthCredentialsStoreMode::File,\n /*chatgpt_base_url*/ None,\n+ AuthKeyringBackendKind::default(),\n )\n .await;\n let mut fresh_auth = remote_control_auth_dot_json(Some(\"account_id\"));\n@@ -547,6 +550,7 @@ a...", + "path": "codex-rs/app-server-transport/src/transport/remote_control/tests/pairing_tests.rs", + "status": "modified" + }, + { + "additions": 8, + "deletions": 0, + "patch_excerpt": "@@ -1821,6 +1821,7 @@ mod tests {\n use codex_config::types::AuthCredentialsStoreMode;\n use codex_core::test_support::auth_manager_from_auth;\n use codex_login::AuthDotJson;\n+ use codex_login::AuthKeyringBackendKind;\n use codex_login::CodexAuth;\n use codex_login::save_auth;\n use codex_login::token_data::TokenData;\n@@ -2202,6 +2203,7 @@ mod tests {\n codex_home.path(),\n &remote_control_auth_dot_json(\"stale-token\"),\n AuthCredentialsStoreMode::File,\n+ AuthKeyringBackendKind::default(),\n )\n .expect(\"stale auth should save\");\n let state_db = remote_control_state_runtime(&codex_home).await;\n@@ -2210,6 +2212,7 @@ mod tests {\n /*enable_codex_api_key_env*/ false,\n AuthCredentialsStoreMode::File,\n /*chatgpt_base_url*/ None,\n+ AuthKeyringBackendKind::default(...", + "path": "codex-rs/app-server-transport/src/transport/remote_control/websocket.rs", + "status": "modified" + }, + { + "additions": 2, + "deletions": 0, + "patch_excerpt": "@@ -293,6 +293,7 @@ impl AccountRequestProcessor {\n &self.config.codex_home,\n ¶ms.api_key,\n self.config.cli_auth_credentials_store_mode,\n+ self.config.auth_keyring_backend_kind(),\n ) {\n Ok(()) => {\n self.auth_manager.reload().await;\n@@ -341,6 +342,7 @@ impl AccountRequestProcessor {\n CLIENT_ID.to_string(),\n config.forced_chatgpt_workspace_id.clone(),\n config.cli_auth_credentials_store_mode,\n+ config.auth_keyring_backend_kind(),\n )\n };\n #[cfg(debug_assertions)]", + "path": "codex-rs/app-server/src/request_processors/account_processor.rs", + "status": "modified" + }, + { + "additions": 8, + "deletions": 1, + "patch_excerpt": "@@ -9,6 +9,7 @@ use chrono::Utc;\n use codex_app_server_protocol::AuthMode;\n use codex_config::types::AuthCredentialsStoreMode;\n use codex_login::AuthDotJson;\n+use codex_login::AuthKeyringBackendKind;\n use codex_login::save_auth;\n use codex_login::token_data::TokenData;\n use codex_login::token_data::parse_chatgpt_jwt_claims;\n@@ -168,5 +169,11 @@ pub fn write_chatgpt_auth(\n bedrock_api_key: None,\n };\n \n- save_auth(codex_home, &auth, cli_auth_credentials_store_mode).context(\"write auth.json\")\n+ save_auth(\n+ codex_home,\n+ &auth,\n+ cli_auth_credentials_store_mode,\n+ AuthKeyringBackendKind::default(),\n+ )\n+ .context(\"write auth.json\")\n }", + "path": "codex-rs/app-server/tests/common/auth_fixtures.rs", + "status": "modified" + }, + { + "additions": 2, + "deletions": 0, + "patch_excerpt": "@@ -33,6 +33,7 @@ use codex_app_server_protocol::ServerRequest;\n use codex_app_server_protocol::TurnCompletedNotification;\n use codex_app_server_protocol::TurnStatus;\n use codex_config::types::AuthCredentialsStoreMode;\n+use codex_login::AuthKeyringBackendKind;\n use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;\n use codex_login::login_with_api_key;\n use codex_protocol::account::PlanType as AccountPlanType;\n@@ -198,6 +199,7 @@ async fn logout_account_removes_auth_and_notifies() -> Result<()> {\n codex_home.path(),\n \"sk-test-key\",\n AuthCredentialsStoreMode::File,\n+ AuthKeyringBackendKind::default(),\n )?;\n assert!(codex_home.path().join(\"auth.json\").exists());", + "path": "codex-rs/app-server/tests/suite/v2/account.rs", + "status": "modified" + }, + { + "additions": 2, + "deletions": 0, + "patch_excerpt": "@@ -35,6 +35,7 @@ use codex_app_server_protocol::ThreadStartParams;\n use codex_app_server_protocol::ThreadStartResponse;\n use codex_config::types::AuthCredentialsStoreMode;\n use codex_login::AuthDotJson;\n+use codex_login::AuthKeyringBackendKind;\n use codex_login::save_auth;\n use pretty_assertions::assert_eq;\n use rmcp::handler::server::ServerHandler;\n@@ -122,6 +123,7 @@ async fn list_apps_returns_empty_with_api_key_auth() -> Result<()> {\n bedrock_api_key: None,\n },\n AuthCredentialsStoreMode::File,\n+ AuthKeyringBackendKind::default(),\n )?;\n \n let mut mcp = TestAppServer::new(codex_home.path()).await?;", + "path": "codex-rs/app-server/tests/suite/v2/app_list.rs", + "status": "modified" + }, + { + "additions": 12, + "deletions": 5, + "patch_excerpt": "@@ -1196,7 +1196,11 @@ fn auth_check(config: &Config) -> DoctorCheck {\n return check;\n }\n \n- match load_auth_dot_json(&config.codex_home, config.cli_auth_credentials_store_mode) {\n+ match load_auth_dot_json(\n+ &config.codex_home,\n+ config.cli_auth_credentials_store_mode,\n+ config.auth_keyring_backend_kind(),\n+ ) {\n Ok(Some(auth)) => {\n details.push(format!(\"stored auth mode: {}\", stored_auth_mode(&auth)));\n details.push(format!(\"stored API key: {}\", auth.openai_api_key.is_some()));\n@@ -2528,10 +2532,13 @@ impl ProviderAuthReachabilityMode {\n }\n \n fn provider_reachability_plan(config: &Config) -> ReachabilityPlan {\n- let stored_auth =\n- load_auth_dot_json(&config.codex_home, config.cli_auth_credentials_store_mode)\n- .ok()\n- .flatten();\n+ let stored_auth = load_auth_dot_json(\n+ ...", + "path": "codex-rs/cli/src/doctor.rs", + "status": "modified" + }, + { + "additions": 56, + "deletions": 10, + "patch_excerpt": "@@ -10,6 +10,7 @@\n use codex_app_server_protocol::AuthMode;\n use codex_config::types::AuthCredentialsStoreMode;\n use codex_core::config::Config;\n+use codex_login::AuthKeyringBackendKind;\n use codex_login::CLIENT_ID;\n use codex_login::CodexAuth;\n use codex_login::ServerOptions;\n@@ -117,8 +118,15 @@ fn print_login_server_start(actual_port: u16, auth_url: &str) {\n async fn clear_existing_auth_before_login(\n codex_home: &Path,\n auth_credentials_store_mode: AuthCredentialsStoreMode,\n+ auth_keyring_backend_kind: AuthKeyringBackendKind,\n ) {\n- if let Err(err) = logout_with_revoke(codex_home, auth_credentials_store_mode).await {\n+ if let Err(err) = logout_with_revoke(\n+ codex_home,\n+ auth_credentials_store_mode,\n+ auth_keyring_backend_kind,\n+ )\n+ .await\n+ {\n tracing::warn!(\"failed to clear existing auth before login: {err}\");\n }\n }\n@@ -1...", + "path": "codex-rs/cli/src/login.rs", + "status": "modified" + }, + { + "additions": 3, + "deletions": 0, + "patch_excerpt": "@@ -5,6 +5,7 @@ use codex_config::CloudConfigBundleLoadError;\n use codex_config::CloudConfigBundleLoadErrorCode;\n use codex_config::CloudConfigBundleLoader;\n use codex_config::types::AuthCredentialsStoreMode;\n+use codex_login::AuthKeyringBackendKind;\n use codex_login::AuthManager;\n use std::path::PathBuf;\n use std::sync::Arc;\n@@ -55,13 +56,15 @@ pub async fn cloud_config_bundle_loader_for_storage(\n codex_home: PathBuf,\n enable_codex_api_key_env: bool,\n credentials_store_mode: AuthCredentialsStoreMode,\n+ keyring_backend_kind: AuthKeyringBackendKind,\n chatgpt_base_url: String,\n ) -> CloudConfigBundleLoader {\n let auth_manager = AuthManager::shared(\n codex_home.clone(),\n enable_codex_api_key_env,\n credentials_store_mode,\n Some(chatgpt_base_url.clone()),\n+ keyring_backend_kind,\n )\n .await;\n cloud_config_bundle_loader(a...", + "path": "codex-rs/cloud-config/src/bundle_loader.rs", + "status": "modified" + }, + { + "additions": 7, + "deletions": 0, + "patch_excerpt": "@@ -16,6 +16,7 @@ use codex_config::CloudConfigTomlBundle;\n use codex_config::CloudRequirementsFragment;\n use codex_config::CloudRequirementsTomlBundle;\n use codex_config::types::AuthCredentialsStoreMode;\n+use codex_login::AuthKeyringBackendKind;\n use pretty_assertions::assert_eq;\n use serde_json::json;\n use std::collections::VecDeque;\n@@ -48,6 +49,7 @@ async fn auth_manager_with_api_key() -> Arc {\n /*enable_codex_api_key_env*/ false,\n AuthCredentialsStoreMode::File,\n /*chatgpt_base_url*/ None,\n+ AuthKeyringBackendKind::default(),\n )\n .await,\n )\n@@ -76,6 +78,7 @@ async fn auth_manager_with_plan_and_identity(\n /*enable_codex_api_key_env*/ false,\n AuthCredentialsStoreMode::File,\n /*chatgpt_base_url*/ None,\n+ AuthKeyringBackendKind::default(),\n )\n .a...", + "path": "codex-rs/cloud-config/src/service_tests.rs", + "status": "modified" + }, + { + "additions": 2, + "deletions": 1, + "patch_excerpt": "@@ -49,7 +49,8 @@ pub async fn load_auth_manager(chatgpt_base_url: Option) -> Option AuthKeyringBackendKind {\n+ Config::auth_keyring_backend_kind(self)\n+ }\n+\n fn forced_chatgpt_workspace_id(&self) -> Option> {\n self.forced_chatgpt_workspace_id.clone()\n }", + "path": "codex-rs/core/src/config/mod.rs", + "status": "modified" + }, + { + "additions": 2, + "deletions": 0, + "patch_excerpt": "@@ -9,6 +9,7 @@ use codex_core::resolve_installation_id;\n use codex_core::thread_store_from_config;\n use codex_extension_api::empty_extension_registry;\n use codex_features::Feature;\n+use codex_login::AuthKeyringBackendKind;\n use codex_login::AuthManager;\n use codex_login::CodexAuth;\n use codex_login::default_client::originator;\n@@ -1178,6 +1179,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {\n codex_home.path(),\n AuthCredentialsStoreMode::File,\n /*chatgpt_base_url*/ None,\n+ AuthKeyringBackendKind::default(),\n )\n .await\n {", + "path": "codex-rs/core/tests/suite/client.rs", + "status": "modified" + }, + { + "additions": 14, + "deletions": 9, + "patch_excerpt": "@@ -62,8 +62,10 @@ use codex_core::check_execpolicy_for_warnings;\n use codex_core::config::Config;\n use codex_core::config::ConfigBuilder;\n use codex_core::config::ConfigOverrides;\n+use codex_core::config::ConfigTomlLoadResult;\n use codex_core::config::find_codex_home;\n-use codex_core::config::load_config_as_toml_with_cli_and_load_options;\n+use codex_core::config::load_config_toml_with_layer_stack;\n+use codex_core::config::resolve_bootstrap_auth_keyring_backend_kind;\n use codex_core::config::resolve_oss_provider;\n use codex_core::config::resolve_profile_v2_config_path;\n use codex_core::find_thread_meta_by_name_str;\n@@ -331,7 +333,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result\n ..Default::default()\n };\n \n- let bootstrap_config_toml = load_config_toml_or_exit(\n+ let bootstrap_config = load_bootstrap_config_or_exit(\n &codex_ho...", + "path": "codex-rs/exec/src/lib.rs", + "status": "modified" + }, + { + "additions": 1, + "deletions": 0, + "patch_excerpt": "@@ -18,6 +18,7 @@ codex-keyring-store = { workspace = true }\n codex-model-provider-info = { workspace = true }\n codex-otel = { workspace = true }\n codex-protocol = { workspace = true }\n+codex-secrets = { workspace = true }\n codex-terminal-detection = { workspace = true }\n codex-utils-template = { workspace = true }\n once_cell = { workspace = true }", + "path": "codex-rs/login/Cargo.toml", + "status": "modified" + }, + { + "additions": 62, + "deletions": 8, + "patch_excerpt": "@@ -43,6 +43,7 @@ async fn refresh_without_id_token() {\n let storage = create_auth_storage(\n codex_home.path().to_path_buf(),\n AuthCredentialsStoreMode::File,\n+ AuthKeyringBackendKind::default(),\n );\n let updated = super::persist_tokens(\n &storage,\n@@ -77,8 +78,13 @@ fn login_with_api_key_overwrites_existing_auth_json() {\n )\n .unwrap();\n \n- super::login_with_api_key(dir.path(), \"sk-new\", AuthCredentialsStoreMode::File)\n- .expect(\"login_with_api_key should succeed\");\n+ super::login_with_api_key(\n+ dir.path(),\n+ \"sk-new\",\n+ AuthCredentialsStoreMode::File,\n+ AuthKeyringBackendKind::default(),\n+ )\n+ .expect(\"login_with_api_key should succeed\");\n \n let storage = FileAuthStorage::new(dir.path().to_path_buf());\n let auth = storage\n@@ -110,6 +116,7 @@ async fn login_with_access_token_writes_...", + "path": "codex-rs/login/src/auth/auth_tests.rs", + "status": "modified" + }, + { + "additions": 8, + "deletions": 1, + "patch_excerpt": "@@ -6,6 +6,7 @@ use serde::Serialize;\n \n use super::manager::save_auth;\n use super::storage::AuthDotJson;\n+use super::storage::AuthKeyringBackendKind;\n use codex_app_server_protocol::AuthMode;\n \n /// Managed Amazon Bedrock API key persisted in `auth.json`.\n@@ -21,6 +22,7 @@ pub fn login_with_bedrock_api_key(\n api_key: &str,\n region: &str,\n auth_credentials_store_mode: AuthCredentialsStoreMode,\n+ keyring_backend_kind: AuthKeyringBackendKind,\n ) -> std::io::Result<()> {\n let auth_dot_json = AuthDotJson {\n auth_mode: Some(AuthMode::BedrockApiKey),\n@@ -34,7 +36,12 @@ pub fn login_with_bedrock_api_key(\n region: region.to_string(),\n }),\n };\n- save_auth(codex_home, &auth_dot_json, auth_credentials_store_mode)\n+ save_auth(\n+ codex_home,\n+ &auth_dot_json,\n+ auth_credentials_store_mode,\n+ keyring_backend_kind,\n+ ...", + "path": "codex-rs/login/src/auth/bedrock_api_key.rs", + "status": "modified" + }, + { + "additions": 8, + "deletions": 0, + "patch_excerpt": "@@ -5,6 +5,7 @@ use serial_test::serial;\n use tempfile::tempdir;\n \n use super::*;\n+use crate::auth::AuthKeyringBackendKind;\n use crate::auth::AuthManager;\n use crate::auth::CodexAuth;\n use crate::auth::storage::AuthStorageBackend;\n@@ -52,13 +53,15 @@ async fn login_with_bedrock_api_key_replaces_openai_auth() -> anyhow::Result<()>\n \"bedrock-api-key-test\",\n \"us-east-1\",\n AuthCredentialsStoreMode::File,\n+ AuthKeyringBackendKind::default(),\n )?;\n \n let auth_manager = AuthManager::new(\n codex_home.path().to_path_buf(),\n /*enable_codex_api_key_env*/ false,\n AuthCredentialsStoreMode::File,\n /*chatgpt_base_url*/ None,\n+ AuthKeyringBackendKind::default(),\n )\n .await;\n \n@@ -98,12 +101,14 @@ async fn logout_removes_bedrock_auth() -> anyhow::Result<()> {\n \"bedrock-api-key-test\",\n \"us-east-1\",\n ...", + "path": "codex-rs/login/src/auth/bedrock_api_key_tests.rs", + "status": "modified" + }, + { + "additions": 118, + "deletions": 15, + "patch_excerpt": "@@ -34,6 +34,7 @@ pub use crate::auth::bedrock_api_key::BedrockApiKeyAuth;\n pub use crate::auth::personal_access_token::PersonalAccessTokenAuth;\n pub use crate::auth::storage::AgentIdentityAuthRecord;\n pub use crate::auth::storage::AuthDotJson;\n+pub use crate::auth::storage::AuthKeyringBackendKind;\n use crate::auth::storage::AuthStorageBackend;\n use crate::auth::storage::create_auth_storage;\n use crate::auth::util::try_parse_error_message;\n@@ -215,6 +216,7 @@ impl CodexAuth {\n auth_dot_json: AuthDotJson,\n auth_credentials_store_mode: AuthCredentialsStoreMode,\n chatgpt_base_url: Option<&str>,\n+ keyring_backend_kind: AuthKeyringBackendKind,\n ) -> std::io::Result {\n let auth_mode = auth_dot_json.resolved_mode();\n let client = create_client();\n@@ -257,7 +259,11 @@ impl CodexAuth {\n \n match auth_mode {\n ApiAuthMode::...", + "path": "codex-rs/login/src/auth/manager.rs", + "status": "modified" + }, + { + "additions": 133, + "deletions": 10, + "patch_excerpt": "@@ -24,9 +24,15 @@ use codex_agent_identity::AgentIdentityJwtClaims;\n use codex_agent_identity::decode_agent_identity_jwt;\n use codex_app_server_protocol::AuthMode;\n use codex_config::types::AuthCredentialsStoreMode;\n+pub use codex_config::types::AuthKeyringBackendKind;\n use codex_keyring_store::DefaultKeyringStore;\n use codex_keyring_store::KeyringStore;\n use codex_protocol::account::PlanType as AccountPlanType;\n+use codex_secrets::LocalSecretsNamespace;\n+use codex_secrets::SecretName;\n+use codex_secrets::SecretScope;\n+use codex_secrets::SecretsBackendKind;\n+use codex_secrets::SecretsManager;\n use once_cell::sync::Lazy;\n \n /// Expected structure for $CODEX_HOME/auth.json.\n@@ -164,6 +170,11 @@ impl AuthStorageBackend for FileAuthStorage {\n }\n }\n \n+static CODEX_AUTH_SECRET_NAME: Lazy =\n+ Lazy::new(|| match SecretName::new(\"CODEX_AUTH\") {\n+ Ok(name) => name,\n+ ...", + "path": "codex-rs/login/src/auth/storage.rs", + "status": "modified" + }, + { + "additions": 237, + "deletions": 68, + "patch_excerpt": "@@ -2,6 +2,11 @@ use super::*;\n use crate::token_data::IdTokenInfo;\n use anyhow::Context;\n use base64::Engine;\n+use codex_secrets::LocalSecretsNamespace;\n+use codex_secrets::SecretScope;\n+use codex_secrets::SecretsBackendKind;\n+use codex_secrets::SecretsManager;\n+use codex_secrets::compute_keyring_account;\n use pretty_assertions::assert_eq;\n use serde_json::json;\n use tempfile::tempdir;\n@@ -152,7 +157,11 @@ fn file_storage_delete_removes_auth_file() -> anyhow::Result<()> {\n personal_access_token: None,\n bedrock_api_key: None,\n };\n- let storage = create_auth_storage(dir.path().to_path_buf(), AuthCredentialsStoreMode::File);\n+ let storage = create_auth_storage(\n+ dir.path().to_path_buf(),\n+ AuthCredentialsStoreMode::File,\n+ AuthKeyringBackendKind::default(),\n+ );\n storage.save(&auth_dot_json)?;\n assert!(dir.path().join(\"auth.json\")....", + "path": "codex-rs/login/src/auth/storage_tests.rs", + "status": "modified" + }, + { + "additions": 1, + "deletions": 0, + "patch_excerpt": "@@ -217,6 +217,7 @@ pub async fn complete_device_code_login(\n tokens.access_token,\n tokens.refresh_token,\n opts.cli_auth_credentials_store_mode,\n+ opts.auth_keyring_backend_kind,\n )\n .await\n }", + "path": "codex-rs/login/src/device_code_auth.rs", + "status": "modified" + }, + { + "additions": 1, + "deletions": 0, + "patch_excerpt": "@@ -19,6 +19,7 @@ pub use server::run_login_server;\n \n pub use auth::AuthConfig;\n pub use auth::AuthDotJson;\n+pub use auth::AuthKeyringBackendKind;\n pub use auth::AuthManager;\n pub use auth::AuthManagerConfig;\n pub use auth::CLIENT_ID;", + "path": "codex-rs/login/src/lib.rs", + "status": "modified" + }, + { + "additions": 12, + "deletions": 1, + "patch_excerpt": "@@ -25,6 +25,7 @@ use std::thread;\n use std::time::Duration;\n \n use crate::auth::AuthDotJson;\n+use crate::auth::AuthKeyringBackendKind;\n use crate::auth::save_auth;\n use crate::default_client::originator;\n use crate::pkce::PkceCodes;\n@@ -69,6 +70,7 @@ pub struct ServerOptions {\n pub forced_chatgpt_workspace_id: Option>,\n pub codex_streamlined_login: bool,\n pub cli_auth_credentials_store_mode: AuthCredentialsStoreMode,\n+ pub auth_keyring_backend_kind: AuthKeyringBackendKind,\n }\n \n impl ServerOptions {\n@@ -78,6 +80,7 @@ impl ServerOptions {\n client_id: String,\n forced_chatgpt_workspace_id: Option>,\n cli_auth_credentials_store_mode: AuthCredentialsStoreMode,\n+ auth_keyring_backend_kind: AuthKeyringBackendKind,\n ) -> Self {\n Self {\n codex_home,\n@@ -89,6 +92,7 @@ impl ServerOptions {\n for...", + "path": "codex-rs/login/src/server.rs", + "status": "modified" + }, + { + "additions": 17, + "deletions": 3, + "patch_excerpt": "@@ -6,6 +6,7 @@ use chrono::Utc;\n use codex_app_server_protocol::AuthMode;\n use codex_config::types::AuthCredentialsStoreMode;\n use codex_login::AuthDotJson;\n+use codex_login::AuthKeyringBackendKind;\n use codex_login::AuthManager;\n use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;\n use codex_login::RefreshTokenError;\n@@ -297,6 +298,7 @@ async fn refresh_token_skips_refresh_when_auth_changed() -> Result<()> {\n ctx.codex_home.path(),\n &disk_auth,\n AuthCredentialsStoreMode::File,\n+ AuthKeyringBackendKind::default(),\n )?;\n \n ctx.auth_manager\n@@ -367,6 +369,7 @@ async fn refresh_token_errors_on_account_mismatch() -> Result<()> {\n ctx.codex_home.path(),\n &disk_auth,\n AuthCredentialsStoreMode::File,\n+ AuthKeyringBackendKind::default(),\n )?;\n \n let err = ctx\n@@ -543,6 +546,7 @@ async fn auth_reloads_disk_auth_when_c...", + "path": "codex-rs/login/tests/suite/auth_refresh.rs", + "status": "modified" + }, + { + "additions": 36, + "deletions": 12, + "patch_excerpt": "@@ -4,6 +4,7 @@ use anyhow::Context;\n use base64::Engine;\n use base64::engine::general_purpose::URL_SAFE_NO_PAD;\n use codex_config::types::AuthCredentialsStoreMode;\n+use codex_login::AuthKeyringBackendKind;\n use codex_login::ServerOptions;\n use codex_login::auth::load_auth_dot_json;\n use codex_login::run_device_code_login;\n@@ -110,6 +111,7 @@ fn server_opts(\n \"client-id\".to_string(),\n /*forced_chatgpt_workspace_id*/ None,\n cli_auth_credentials_store_mode,\n+ AuthKeyringBackendKind::default(),\n );\n opts.issuer = issuer;\n opts.open_browser = false;\n@@ -147,9 +149,13 @@ async fn device_code_login_integration_succeeds() -> anyhow::Result<()> {\n .await\n .expect(\"device code login integration should succeed\");\n \n- let auth = load_auth_dot_json(codex_home.path(), AuthCredentialsStoreMode::File)\n- .context(\"auth.json should load...", + "path": "codex-rs/login/tests/suite/device_code_login.rs", + "status": "modified" + }, + { + "additions": 10, + "deletions": 0, + "patch_excerpt": "@@ -9,6 +9,7 @@ use std::time::Duration;\n use anyhow::Result;\n use base64::Engine;\n use codex_config::types::AuthCredentialsStoreMode;\n+use codex_login::AuthKeyringBackendKind;\n use codex_login::ServerOptions;\n use codex_login::run_login_server;\n use core_test_support::skip_if_no_network;\n@@ -128,6 +129,7 @@ async fn end_to_end_login_flow_persists_auth_json() -> Result<()> {\n force_state: Some(state),\n forced_chatgpt_workspace_id: Some(vec![chatgpt_account_id.to_string()]),\n codex_streamlined_login: false,\n+ auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,\n };\n let server = run_login_server(opts)?;\n assert!(\n@@ -190,6 +192,7 @@ async fn creates_missing_codex_home_dir() -> Result<()> {\n force_state: Some(state),\n forced_chatgpt_workspace_id: None,\n codex_streamlined_login: false,\n+ auth_keyring_backend_kin...", + "path": "codex-rs/login/tests/suite/login_server_e2e.rs", + "status": "modified" + }, + { + "additions": 25, + "deletions": 3, + "patch_excerpt": "@@ -4,6 +4,7 @@ use base64::Engine;\n use codex_app_server_protocol::AuthMode;\n use codex_config::types::AuthCredentialsStoreMode;\n use codex_login::AuthDotJson;\n+use codex_login::AuthKeyringBackendKind;\n use codex_login::AuthManager;\n use codex_login::CLIENT_ID;\n use codex_login::CODEX_ACCESS_TOKEN_ENV_VAR;\n@@ -51,9 +52,15 @@ async fn logout_with_revoke_revokes_refresh_token_then_removes_auth() -> Result<\n codex_home.path(),\n &chatgpt_auth(),\n AuthCredentialsStoreMode::File,\n+ AuthKeyringBackendKind::default(),\n )?;\n \n- let removed = logout_with_revoke(codex_home.path(), AuthCredentialsStoreMode::File).await?;\n+ let removed = logout_with_revoke(\n+ codex_home.path(),\n+ AuthCredentialsStoreMode::File,\n+ AuthKeyringBackendKind::default(),\n+ )\n+ .await?;\n \n assert!(removed);\n assert!(!codex_home.path().join(\"auth.jso...", + "path": "codex-rs/login/tests/suite/logout.rs", + "status": "modified" + }, + { + "additions": 2, + "deletions": 0, + "patch_excerpt": "@@ -3,6 +3,7 @@ use crate::ModelsManagerConfig;\n use chrono::Utc;\n use codex_app_server_protocol::AuthMode;\n use codex_login::AuthCredentialsStoreMode;\n+use codex_login::AuthKeyringBackendKind;\n use codex_login::AuthManager;\n use codex_login::CodexAuth;\n use codex_login::ExternalAuth;\n@@ -230,6 +231,7 @@ c2ln\",\n codex_home,\n AuthCredentialsStoreMode::File,\n /*chatgpt_base_url*/ None,\n+ AuthKeyringBackendKind::default(),\n )\n .await\n .expect(\"auth should load\")", + "path": "codex-rs/models-manager/src/manager_tests.rs", + "status": "modified" + }, + { + "additions": 15, + "deletions": 9, + "patch_excerpt": "@@ -7,7 +7,9 @@ use crate::legacy_core::check_execpolicy_for_warnings;\n use crate::legacy_core::config::Config;\n use crate::legacy_core::config::ConfigBuilder;\n use crate::legacy_core::config::ConfigOverrides;\n-use crate::legacy_core::config::load_config_as_toml_with_cli_and_load_options;\n+use crate::legacy_core::config::ConfigTomlLoadResult;\n+use crate::legacy_core::config::load_config_toml_with_layer_stack;\n+use crate::legacy_core::config::resolve_bootstrap_auth_keyring_backend_kind;\n use crate::legacy_core::config::resolve_oss_provider;\n use crate::legacy_core::config::resolve_profile_v2_config_path;\n use crate::legacy_core::format_exec_policy_error_with_source;\n@@ -1012,7 +1014,7 @@ pub async fn run_main(\n loader_overrides.user_config_profile = Some(profile_v2.clone());\n }\n \n- let bootstrap_config_toml = load_config_toml_or_exit(\n+ let bootstrap_config = load_bootst...", + "path": "codex-rs/tui/src/lib.rs", + "status": "modified" + }, + { + "additions": 16, + "deletions": 5, + "patch_excerpt": "@@ -4,6 +4,7 @@ use std::path::Path;\n \n use codex_app_server_protocol::AuthMode;\n use codex_config::types::AuthCredentialsStoreMode;\n+use codex_login::AuthKeyringBackendKind;\n use codex_login::load_auth_dot_json;\n \n #[derive(Debug, Clone, PartialEq, Eq)]\n@@ -18,9 +19,13 @@ pub(crate) fn load_local_chatgpt_auth(\n auth_credentials_store_mode: AuthCredentialsStoreMode,\n forced_chatgpt_workspace_id: Option<&[String]>,\n ) -> Result {\n- let auth = load_auth_dot_json(codex_home, auth_credentials_store_mode)\n- .map_err(|err| format!(\"failed to load local auth: {err}\"))?\n- .ok_or_else(|| \"no local auth available\".to_string())?;\n+ let auth = load_auth_dot_json(\n+ codex_home,\n+ auth_credentials_store_mode,\n+ AuthKeyringBackendKind::default(),\n+ )\n+ .map_err(|err| format!(\"failed to load local auth: {err}\"))?\n+ .ok_o...", + "path": "codex-rs/tui/src/local_chatgpt_auth.rs", + "status": "modified" + }, + { + "additions": 2, + "deletions": 0, + "patch_excerpt": "@@ -1014,6 +1014,7 @@ mod tests {\n use codex_arg0::Arg0DispatchPaths;\n use codex_cloud_config::cloud_config_bundle_loader_for_storage;\n use codex_config::types::AuthCredentialsStoreMode;\n+ use codex_login::AuthKeyringBackendKind;\n \n use pretty_assertions::assert_eq;\n use std::sync::Arc;\n@@ -1037,6 +1038,7 @@ mod tests {\n codex_home_path.clone(),\n /*enable_codex_api_key_env*/ false,\n AuthCredentialsStoreMode::File,\n+ AuthKeyringBackendKind::default(),\n \"https://chatgpt.com/backend-api/\".to_string(),\n )\n .await,", + "path": "codex-rs/tui/src/onboarding/auth.rs", + "status": "modified" + }, + { + "additions": 6, + "deletions": 3, + "patch_excerpt": "@@ -11,7 +11,8 @@ use crate::Cli;\n use crate::app_server_session::AppServerSession;\n use crate::legacy_core::config::ConfigBuilder;\n use crate::legacy_core::config::ConfigOverrides;\n-use crate::legacy_core::config::load_config_as_toml_with_cli_and_load_options;\n+use crate::legacy_core::config::load_config_toml_with_layer_stack;\n+use crate::legacy_core::config::resolve_bootstrap_auth_keyring_backend_kind;\n use crate::legacy_core::config::resolve_oss_provider;\n use crate::legacy_core::config::resolve_profile_v2_config_path;\n use codex_app_server_protocol::Thread as AppServerThread;\n@@ -310,7 +311,7 @@ async fn start_app_server_for_archive_command(\n loader_overrides.user_config_profile = Some(profile_v2.clone());\n }\n \n- let config_toml = load_config_as_toml_with_cli_and_load_options(\n+ let bootstrap_config = load_config_toml_with_layer_stack(\n codex_home.as_path(),...", + "path": "codex-rs/tui/src/session_archive_commands.rs", + "status": "modified" + } + ], + "linked_issues": [ + "#27504", + "#27535" + ], + "notes": [ + "Built from GitHub pull-request, commits, files, and repo endpoints." + ], + "primary_pr": { + "body": "## Why\n\nWindows Credential Manager limits generic credential blobs to 2,560 bytes. Large serialized ChatGPT auth payloads can exceed that limit, so keyring-mode CLI auth needs a backend that keeps only the encryption key in the OS keyring and stores the payload in Codex's encrypted local-secrets file.\n\nThis is the third PR in the encrypted-auth stack:\n\n1. #27504 — feature and config selection\n2. #27535 — auth-specific local-secrets namespaces\n3. This PR — CLI auth implementation and activation\n4. MCP OAuth implementation and activation\n\n## What Changed\n\n- Added encrypted CLI-auth storage using the `CliAuth` secrets namespace.\n- Preserved direct keyring storage for platforms/configurations where it remains selected.\n- Selected the backend consistently for login, logout, refresh, device-code login, auth loading, and login restrictions.\n- Threaded resolved bootstrap/full config through CLI, exec, TUI, app-server account handling, cloud config, and cloud tasks.\n- Removed stale `auth.json` fallback data after successful encrypted saves and removed encrypted, direct-keyring, and fallback data during logout.\n- Added storage and integration coverage for both direct and encrypted keyring modes.\n\nMCP OAuth persistence is intentionally left to the next PR.\n\n## Validation\n\n- `just test -p codex-login` — 131 passed\n- `just test -p codex-cli` — 280 passed\n- `just test -p codex-app-server v2::account` — 25 passed\n- `just test -p codex-cloud-config service` — 21 passed, 7 skipped\n- `just fix -p codex-login`\n- `just fix -p codex-cli`\n- `just fmt`\n", + "labels": [], + "merged_at": "2026-06-12T21:23:50Z", + "number": 27539, + "state": "merged", + "title": "feat: use encrypted local secrets for CLI auth", + "url": "https://github.com/openai/codex/pull/27539" + }, + "repo": "openai/codex", + "schema": "github_change_bundle/v1" +} diff --git a/artifacts/github/bundles/openai-codex-pr-27936.json b/artifacts/github/bundles/openai-codex-pr-27936.json new file mode 100644 index 000000000..f8a7c3b97 --- /dev/null +++ b/artifacts/github/bundles/openai-codex-pr-27936.json @@ -0,0 +1,187 @@ +{ + "analysis_mode": "pr_first", + "commits": [ + { + "author": "agamble-oai", + "committed_at": "2026-06-12T18:58:56Z", + "message": "add roles to realtime append text", + "sha": "728db43093fb3390778de642164be5520aa9262a", + "url": "https://github.com/openai/codex/commit/728db43093fb3390778de642164be5520aa9262a" + } + ], + "default_branch": "main", + "docs_refs": [ + "codex-rs/app-server/README.md" + ], + "examples_refs": [], + "extracted_flags": [ + "JSON", + "API", + "GENERATED", + "CODE", + "NOT", + "MODIFY", + "HAND", + "EXPERIMENTAL", + "SDP", + "V2U", + "FEATURES", + "JSONRPCR", + "REALTIME_V2_OUTPUT_MODALITY_AUDIO", + "REALTIME_V2_SILENCE_TOOL_NAME", + "REALTIME_V2_SILENCE_TOOL_DESCRIPTION", + "REALTIME_V2_INPUT_TRANSCRIPTION_MODEL", + "AUDIO_IN_QUEUE_CAPACITY", + "USER_TEXT_IN_QUEUE_CAPACITY", + "TEXT_IN_QUEUE_CAPACITY", + "HANDOFF_OUT_QUEUE_CAPACITY", + "OUTPUT_EVENTS_QUEUE_CAPACITY", + "REALTIME_STARTUP_CONTEXT_TOKEN_BUDGET", + "REALTIME_USER_TEXT_PREFIX" + ], + "files": [ + { + "additions": 7, + "deletions": 0, + "patch_excerpt": "@@ -608,6 +608,13 @@\n }\n ]\n },\n+ \"ConversationTextRole\": {\n+ \"enum\": [\n+ \"user\",\n+ \"developer\"\n+ ],\n+ \"type\": \"string\"\n+ },\n \"DynamicToolSpec\": {\n \"properties\": {\n \"deferLoading\": {", + "path": "codex-rs/app-server-protocol/schema/json/ClientRequest.json", + "status": "modified" + }, + { + "additions": 7, + "deletions": 0, + "patch_excerpt": "@@ -8519,6 +8519,13 @@\n \"title\": \"ContextCompactedNotification\",\n \"type\": \"object\"\n },\n+ \"ConversationTextRole\": {\n+ \"enum\": [\n+ \"user\",\n+ \"developer\"\n+ ],\n+ \"type\": \"string\"\n+ },\n \"CreditsSnapshot\": {\n \"properties\": {\n \"balance\": {", + "path": "codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json", + "status": "modified" + }, + { + "additions": 7, + "deletions": 0, + "patch_excerpt": "@@ -4832,6 +4832,13 @@\n \"title\": \"ContextCompactedNotification\",\n \"type\": \"object\"\n },\n+ \"ConversationTextRole\": {\n+ \"enum\": [\n+ \"user\",\n+ \"developer\"\n+ ],\n+ \"type\": \"string\"\n+ },\n \"CreditsSnapshot\": {\n \"properties\": {\n \"balance\": {", + "path": "codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json", + "status": "modified" + }, + { + "additions": 5, + "deletions": 0, + "patch_excerpt": "@@ -0,0 +1,5 @@\n+// GENERATED CODE! DO NOT MODIFY BY HAND!\n+\n+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n+\n+export type ConversationTextRole = \"user\" | \"developer\";", + "path": "codex-rs/app-server-protocol/schema/typescript/ConversationTextRole.ts", + "status": "added" + }, + { + "additions": 1, + "deletions": 0, + "patch_excerpt": "@@ -14,6 +14,7 @@ export type { CollaborationMode } from \"./CollaborationMode\";\n export type { ContentItem } from \"./ContentItem\";\n export type { ConversationGitInfo } from \"./ConversationGitInfo\";\n export type { ConversationSummary } from \"./ConversationSummary\";\n+export type { ConversationTextRole } from \"./ConversationTextRole\";\n export type { ExecCommandApprovalParams } from \"./ExecCommandApprovalParams\";\n export type { ExecCommandApprovalResponse } from \"./ExecCommandApprovalResponse\";\n export type { ExecPolicyAmendment } from \"./ExecPolicyAmendment\";", + "path": "codex-rs/app-server-protocol/schema/typescript/index.ts", + "status": "modified" + }, + { + "additions": 3, + "deletions": 0, + "patch_excerpt": "@@ -1,3 +1,4 @@\n+use codex_protocol::protocol::ConversationTextRole;\n use codex_protocol::protocol::RealtimeAudioFrame as CoreRealtimeAudioFrame;\n use codex_protocol::protocol::RealtimeConversationVersion;\n use codex_protocol::protocol::RealtimeOutputModality;\n@@ -131,6 +132,8 @@ pub struct ThreadRealtimeAppendAudioResponse {}\n pub struct ThreadRealtimeAppendTextParams {\n pub thread_id: String,\n pub text: String,\n+ #[serde(default)]\n+ pub role: ConversationTextRole,\n }\n \n /// EXPERIMENTAL - response for appending realtime text input.", + "path": "codex-rs/app-server-protocol/src/protocol/v2/realtime.rs", + "status": "modified" + }, + { + "additions": 19, + "deletions": 0, + "patch_excerpt": "@@ -27,6 +27,7 @@ use codex_protocol::permissions::FileSystemSandboxEntry as CoreFileSystemSandbox\n use codex_protocol::permissions::FileSystemSpecialPath as CoreFileSystemSpecialPath;\n use codex_protocol::protocol::AgentStatus as CoreAgentStatus;\n use codex_protocol::protocol::AskForApproval as CoreAskForApproval;\n+use codex_protocol::protocol::ConversationTextRole;\n use codex_protocol::protocol::GranularApprovalConfig as CoreGranularApprovalConfig;\n use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;\n use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile;\n@@ -3862,3 +3863,21 @@ fn turn_start_params_reject_relative_environment_cwd() {\n \"unexpected error: {err}\"\n );\n }\n+\n+#[test]\n+fn realtime_append_text_defaults_role_to_user() {\n+ let params = serde_json::from_value::(json!({\n+ ...", + "path": "codex-rs/app-server-protocol/src/protocol/v2/tests.rs", + "status": "modified" + }, + { + "additions": 1, + "deletions": 1, + "patch_excerpt": "@@ -167,7 +167,7 @@ Example with notification opt-out:\n - `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: \"interrupted\"`.\n - `thread/realtime/start` — start a thread-scoped realtime session (experimental); pass `outputModality: \"text\"` or `outputModality: \"audio\"` to choose model output, and optionally pass `model` and `version` to override configured realtime selection for this session only. Returns `{}` and streams `thread/realtime/*` notifications. Omit `transport` for the websocket transport, or pass `{ \"type\": \"webrtc\", \"sdp\": \"...\" }` to create a WebRTC session from a browser-generated SDP offer; the remote answer SDP is emitted as `thread/realtime/sdp`.\n - `thread/realtime/appendAudio` — append an input audio chunk to the active realtime session (experimental); returns `{}...", + "path": "codex-rs/app-server/README.md", + "status": "modified" + }, + { + "additions": 4, + "deletions": 1, + "patch_excerpt": "@@ -996,7 +996,10 @@ impl TurnRequestProcessor {\n self.submit_core_op(\n request_id,\n thread.as_ref(),\n- Op::RealtimeConversationText(ConversationTextParams { text: params.text }),\n+ Op::RealtimeConversationText(ConversationTextParams {\n+ text: params.text,\n+ role: params.role,\n+ }),\n )\n .await\n .map_err(|err| {", + "path": "codex-rs/app-server/src/request_processors/turn_processor.rs", + "status": "modified" + }, + { + "additions": 22, + "deletions": 0, + "patch_excerpt": "@@ -42,6 +42,7 @@ use codex_app_server_protocol::TurnStartedNotification;\n use codex_app_server_protocol::UserInput as V2UserInput;\n use codex_features::FEATURES;\n use codex_features::Feature;\n+use codex_protocol::protocol::ConversationTextRole;\n use codex_protocol::protocol::RealtimeConversationVersion;\n use codex_protocol::protocol::RealtimeOutputModality;\n use codex_protocol::protocol::RealtimeVoice;\n@@ -392,6 +393,7 @@ impl RealtimeE2eHarness {\n .send_thread_realtime_append_text_request(ThreadRealtimeAppendTextParams {\n thread_id,\n text: text.to_string(),\n+ role: ConversationTextRole::User,\n })\n .await?;\n let response: JSONRPCResponse = timeout(\n@@ -634,6 +636,7 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> {\n .send_thread_realtime_append_text_request(Thread...", + "path": "codex-rs/app-server/tests/suite/v2/realtime_conversation.rs", + "status": "modified" + }, + { + "additions": 28, + "deletions": 7, + "patch_excerpt": "@@ -17,6 +17,7 @@ use crate::error::ApiError;\n use crate::provider::Provider;\n use codex_client::backoff;\n use codex_client::maybe_build_rustls_client_config_with_custom_ca;\n+use codex_protocol::protocol::ConversationTextRole;\n use codex_protocol::protocol::RealtimeTranscriptDelta;\n use codex_utils_rustls_provider::ensure_rustls_crypto_provider;\n use futures::SinkExt;\n@@ -227,8 +228,12 @@ impl RealtimeWebsocketConnection {\n self.writer.send_audio_frame(frame).await\n }\n \n- pub async fn send_conversation_item_create(&self, text: String) -> Result<(), ApiError> {\n- self.writer.send_conversation_item_create(text).await\n+ pub async fn send_conversation_item_create(\n+ &self,\n+ text: String,\n+ role: ConversationTextRole,\n+ ) -> Result<(), ApiError> {\n+ self.writer.send_conversation_item_create(text, role).await\n }\n \n pub async fn s...", + "path": "codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs", + "status": "modified" + }, + { + "additions": 4, + "deletions": 2, + "patch_excerpt": "@@ -13,6 +13,7 @@ use crate::endpoint::realtime_websocket::protocol::RealtimeSessionConfig;\n use crate::endpoint::realtime_websocket::protocol::RealtimeSessionMode;\n use crate::endpoint::realtime_websocket::protocol::RealtimeVoice;\n use crate::endpoint::realtime_websocket::protocol::SessionUpdateSession;\n+use codex_protocol::protocol::ConversationTextRole;\n use serde_json::Result as JsonResult;\n use serde_json::Value;\n use serde_json::to_value;\n@@ -33,10 +34,11 @@ pub(super) fn normalized_session_mode(\n pub(super) fn conversation_item_create_message(\n event_parser: RealtimeEventParser,\n text: String,\n+ role: ConversationTextRole,\n ) -> RealtimeOutboundMessage {\n match event_parser {\n- RealtimeEventParser::V1 => v1_conversation_item_create_message(text),\n- RealtimeEventParser::RealtimeV2 => v2_conversation_item_create_message(text),\n+ RealtimeEventParse...", + "path": "codex-rs/codex-api/src/endpoint/realtime_websocket/methods_common.rs", + "status": "modified" + }, + { + "additions": 6, + "deletions": 3, + "patch_excerpt": "@@ -5,7 +5,6 @@ use crate::endpoint::realtime_websocket::protocol::ConversationItemContent;\n use crate::endpoint::realtime_websocket::protocol::ConversationItemPayload;\n use crate::endpoint::realtime_websocket::protocol::ConversationItemType;\n use crate::endpoint::realtime_websocket::protocol::ConversationMessageItem;\n-use crate::endpoint::realtime_websocket::protocol::ConversationRole;\n use crate::endpoint::realtime_websocket::protocol::RealtimeOutboundMessage;\n use crate::endpoint::realtime_websocket::protocol::RealtimeVoice;\n use crate::endpoint::realtime_websocket::protocol::SessionAudio;\n@@ -14,12 +13,16 @@ use crate::endpoint::realtime_websocket::protocol::SessionAudioInput;\n use crate::endpoint::realtime_websocket::protocol::SessionAudioOutput;\n use crate::endpoint::realtime_websocket::protocol::SessionType;\n use crate::endpoint::realtime_websocket::protocol::SessionUpdateSession;...", + "path": "codex-rs/codex-api/src/endpoint/realtime_websocket/methods_v1.rs", + "status": "modified" + }, + { + "additions": 6, + "deletions": 3, + "patch_excerpt": "@@ -6,7 +6,6 @@ use crate::endpoint::realtime_websocket::protocol::ConversationItemContent;\n use crate::endpoint::realtime_websocket::protocol::ConversationItemPayload;\n use crate::endpoint::realtime_websocket::protocol::ConversationItemType;\n use crate::endpoint::realtime_websocket::protocol::ConversationMessageItem;\n-use crate::endpoint::realtime_websocket::protocol::ConversationRole;\n use crate::endpoint::realtime_websocket::protocol::NoiseReductionType;\n use crate::endpoint::realtime_websocket::protocol::RealtimeOutboundMessage;\n use crate::endpoint::realtime_websocket::protocol::RealtimeOutputModality;\n@@ -25,6 +24,7 @@ use crate::endpoint::realtime_websocket::protocol::SessionTurnDetection;\n use crate::endpoint::realtime_websocket::protocol::SessionType;\n use crate::endpoint::realtime_websocket::protocol::SessionUpdateSession;\n use crate::endpoint::realtime_websocket::protocol::Tur...", + "path": "codex-rs/codex-api/src/endpoint/realtime_websocket/methods_v2.rs", + "status": "modified" + }, + { + "additions": 2, + "deletions": 7, + "patch_excerpt": "@@ -1,5 +1,6 @@\n use crate::endpoint::realtime_websocket::protocol_v1::parse_realtime_event_v1;\n use crate::endpoint::realtime_websocket::protocol_v2::parse_realtime_event_v2;\n+use codex_protocol::protocol::ConversationTextRole;\n pub use codex_protocol::protocol::RealtimeAudioFrame;\n pub use codex_protocol::protocol::RealtimeEvent;\n pub use codex_protocol::protocol::RealtimeOutputModality;\n@@ -157,7 +158,7 @@ pub(super) struct SessionAudioOutputFormat {\n pub(super) struct ConversationMessageItem {\n #[serde(rename = \"type\")]\n pub(super) r#type: ConversationItemType,\n- pub(super) role: ConversationRole,\n+ pub(super) role: ConversationTextRole,\n pub(super) content: Vec,\n }\n \n@@ -168,12 +169,6 @@ pub(super) enum ConversationItemType {\n FunctionCallOutput,\n }\n \n-#[derive(Debug, Clone, Copy, Serialize)]\n-#[serde(rename_all = \"snake_case\")]\n-pub(su...", + "path": "codex-rs/codex-api/src/endpoint/realtime_websocket/protocol.rs", + "status": "modified" + }, + { + "additions": 38, + "deletions": 26, + "patch_excerpt": "@@ -35,6 +35,7 @@ use codex_protocol::protocol::ConversationAudioParams;\n use codex_protocol::protocol::ConversationStartParams;\n use codex_protocol::protocol::ConversationStartTransport;\n use codex_protocol::protocol::ConversationTextParams;\n+use codex_protocol::protocol::ConversationTextRole;\n use codex_protocol::protocol::ErrorEvent;\n use codex_protocol::protocol::Event;\n use codex_protocol::protocol::EventMsg;\n@@ -61,7 +62,7 @@ use tracing::info;\n use tracing::warn;\n \n const AUDIO_IN_QUEUE_CAPACITY: usize = 256;\n-const USER_TEXT_IN_QUEUE_CAPACITY: usize = 64;\n+const TEXT_IN_QUEUE_CAPACITY: usize = 64;\n const HANDOFF_OUT_QUEUE_CAPACITY: usize = 64;\n const OUTPUT_EVENTS_QUEUE_CAPACITY: usize = 256;\n const REALTIME_STARTUP_CONTEXT_TOKEN_BUDGET: usize = 5_300;\n@@ -193,7 +194,7 @@ impl RealtimeResponseCreateQueue {\n struct RealtimeInputTask {\n writer: RealtimeWebsocketWriter,\n eve...", + "path": "codex-rs/core/src/realtime_conversation.rs", + "status": "modified" + }, + { + "additions": 6, + "deletions": 0, + "patch_excerpt": "@@ -13,6 +13,7 @@ use codex_protocol::protocol::ConversationAudioParams;\n use codex_protocol::protocol::ConversationStartParams;\n use codex_protocol::protocol::ConversationStartTransport;\n use codex_protocol::protocol::ConversationTextParams;\n+use codex_protocol::protocol::ConversationTextRole;\n use codex_protocol::protocol::ErrorEvent;\n use codex_protocol::protocol::EventMsg;\n use codex_protocol::protocol::InitialHistory;\n@@ -329,6 +330,7 @@ async fn conversation_start_audio_text_close_round_trip() -> Result<()> {\n test.codex\n .submit(Op::RealtimeConversationText(ConversationTextParams {\n text: \"hello\".to_string(),\n+ role: ConversationTextRole::User,\n }))\n .await?;\n \n@@ -542,6 +544,7 @@ async fn conversation_webrtc_start_posts_generated_session() -> Result<()> {\n test.codex\n .submit(Op::RealtimeConversationText(ConversationT...", + "path": "codex-rs/core/tests/suite/realtime_conversation.rs", + "status": "modified" + }, + { + "additions": 10, + "deletions": 0, + "patch_excerpt": "@@ -398,6 +398,16 @@ pub struct ConversationAudioParams {\n #[derive(Debug, Clone, PartialEq)]\n pub struct ConversationTextParams {\n pub text: String,\n+ pub role: ConversationTextRole,\n+}\n+\n+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize, JsonSchema, TS)]\n+#[serde(rename_all = \"snake_case\")]\n+#[ts(rename_all = \"snake_case\")]\n+pub enum ConversationTextRole {\n+ #[default]\n+ User,\n+ Developer,\n }\n \n /// Persistent thread-settings overrides that can be applied before user input or", + "path": "codex-rs/protocol/src/protocol.rs", + "status": "modified" + } + ], + "linked_issues": [ + "openai/openai#1025261" + ], + "notes": [ + "Built from GitHub pull-request, commits, files, and repo endpoints." + ], + "primary_pr": { + "body": "## Summary\n\nAdd an explicit `user` or `developer` role to `thread/realtime/appendText` and propagate it through the realtime input queue into `conversation.item.create`. Older JSON clients that omit the field continue to default to `user`.\n\nThis lets app-provided context such as memory retain developer authority without bypassing app-server through a renderer-owned data channel. The app-server schemas, API documentation, and focused protocol and websocket coverage are updated with the new contract.\n\nThe Codex Apps consumer is tracked in [openai/openai#1025261](https://github.com/openai/openai/pull/1025261).\n", + "labels": [], + "merged_at": "2026-06-12T22:05:38Z", + "number": 27936, + "state": "merged", + "title": "[codex] add roles to realtime append text", + "url": "https://github.com/openai/codex/pull/27936" + }, + "repo": "openai/codex", + "schema": "github_change_bundle/v1" +} diff --git a/artifacts/github/impact/openai-codex-pr-27535.json b/artifacts/github/impact/openai-codex-pr-27535.json new file mode 100644 index 000000000..7fec178eb --- /dev/null +++ b/artifacts/github/impact/openai-codex-pr-27535.json @@ -0,0 +1,45 @@ +{ + "candidate_followups": [ + "Keep PR #27535 grouped with the encrypted-auth stack when explaining or auditing later CLI auth and MCP OAuth behavior.", + "When Decodex account tooling reads or migrates Codex credentials, treat `codex_auth.age` and `mcp_oauth.age` as distinct local-secrets namespaces instead of assuming one encrypted secrets file.", + "Wait for activation PRs before claiming user-visible encrypted auth behavior from this PR alone." + ], + "caveats": [ + "The PR body says this does not activate CLI auth or MCP OAuth encrypted storage.", + "Standalone public publishing would overstate the change; it is better used as context for #27539 and the MCP OAuth activation PR." + ], + "confidence": "confirmed", + "control_plane_impact": "watch", + "evidence": [ + "The source-backed review is recorded at `artifacts/github/reviews/openai-codex-pr-27535.review.json`.", + "`secret_auth_storage` is added as a stable feature/config key and defaults to Windows only.", + "`LocalSecretsNamespace` separates managed secrets, Codex auth, and MCP OAuth into different encrypted local files.", + "The PR explicitly states that it does not activate auth backends or change existing credential behavior." + ], + "observed_change": "Codex adds auth-specific encrypted local-secrets namespaces and a `secret_auth_storage` config/feature path, without activating new credential storage behavior yet.", + "public_signal_decision": "defer", + "publisher_angle": "watch_note", + "repo": "openai/codex", + "schema": "upstream_impact/v1", + "slug": "openai-codex-pr-27535", + "social_notes": [ + "Do not publish #27535 as a standalone feature claim.", + "Use it as source-backed context when Publisher evaluates encrypted CLI auth or MCP OAuth activation artifacts." + ], + "source_refs": { + "items": [ + { + "kind": "pull_request", + "meta": "Merged 2026-06-12T19:52:50Z", + "title": "feat: add auth-specific encrypted secret namespaces", + "url": "https://github.com/openai/codex/pull/27535" + }, + { + "kind": "pull_request", + "meta": "artifacts/github/reviews/openai-codex-pr-27535.review.json", + "title": "Source-backed Decodex upstream review", + "url": "https://github.com/openai/codex/pull/27535" + } + ] + } +} diff --git a/artifacts/github/impact/openai-codex-pr-27539.json b/artifacts/github/impact/openai-codex-pr-27539.json new file mode 100644 index 000000000..12af0ba43 --- /dev/null +++ b/artifacts/github/impact/openai-codex-pr-27539.json @@ -0,0 +1,47 @@ +{ + "candidate_followups": [ + "Audit Decodex App account switching and local account-pool refresh logic for file-only `$CODEX_HOME/auth.json` assumptions.", + "Prefer app-server/account APIs or upstream auth-manager semantics when validating active Codex auth after this upstream change.", + "Treat missing `auth.json` as inconclusive when `secret_auth_storage` can route CLI auth into encrypted local secrets." + ], + "caveats": [ + "Direct keyring storage still exists where selected.", + "The encrypted local-secrets backend depends on the resolved `AuthKeyringBackendKind`; PR #27535 makes the `secret_auth_storage` default Windows-specific.", + "This PR leaves MCP OAuth persistence to a separate activation PR." + ], + "confidence": "confirmed", + "control_plane_impact": "compat_risk", + "evidence": [ + "The source-backed review is recorded at `artifacts/github/reviews/openai-codex-pr-27539.review.json`.", + "PR #27539 describes encrypted CLI-auth storage using the `CliAuth` secrets namespace for large serialized ChatGPT auth payloads.", + "`codex-rs/login/src/auth/storage.rs` wires auth storage to local secrets and `AuthKeyringBackendKind`.", + "`codex-rs/app-server/src/request_processors/account_processor.rs` passes `config.auth_keyring_backend_kind()` for API-key save and login-server account handling.", + "The PR body says stale `auth.json` fallback data is removed after encrypted saves and that logout removes encrypted, direct-keyring, and fallback data." + ], + "observed_change": "Codex activates encrypted local-secrets storage for CLI auth payloads when keyring-mode auth resolves to the `Secrets` backend.", + "public_signal_decision": "publish", + "publisher_angle": "operator_impact", + "repo": "openai/codex", + "schema": "upstream_impact/v1", + "slug": "openai-codex-pr-27539", + "social_notes": [ + "Frame this as an auth-storage assumption change for operators, not as a broad auth UX redesign.", + "Mention that large ChatGPT auth payloads can move out of direct keyring blobs and that `auth.json` may no longer be the sole durable source." + ], + "source_refs": { + "items": [ + { + "kind": "pull_request", + "meta": "Merged 2026-06-12T21:23:50Z", + "title": "feat: use encrypted local secrets for CLI auth", + "url": "https://github.com/openai/codex/pull/27539" + }, + { + "kind": "pull_request", + "meta": "artifacts/github/reviews/openai-codex-pr-27539.review.json", + "title": "Source-backed Decodex upstream review", + "url": "https://github.com/openai/codex/pull/27539" + } + ] + } +} diff --git a/artifacts/github/impact/openai-codex-pr-27936.json b/artifacts/github/impact/openai-codex-pr-27936.json new file mode 100644 index 000000000..13102863f --- /dev/null +++ b/artifacts/github/impact/openai-codex-pr-27936.json @@ -0,0 +1,47 @@ +{ + "candidate_followups": [ + "Update any Decodex app-server realtime client or generated protocol bindings to include the optional `role` field.", + "Use developer role only for trusted app-provided context such as memory or durable operator instructions.", + "Keep ordinary operator/user input on the default `user` role." + ], + "caveats": [ + "The field defaults to `user`, so this is primarily an adoption opportunity rather than a hard break for JSON clients.", + "Developer-role text changes authority semantics; integrations should not route arbitrary user text through it." + ], + "confidence": "confirmed", + "control_plane_impact": "candidate", + "evidence": [ + "The source-backed review is recorded at `artifacts/github/reviews/openai-codex-pr-27936.review.json`.", + "`ThreadRealtimeAppendTextParams` now carries `role: ConversationTextRole` with serde defaulting.", + "`ConversationTextRole` serializes as `user` or `developer` and defaults to user.", + "The app-server request processor forwards the role into realtime conversation text params.", + "Realtime websocket outbound conversation item creation serializes the selected role." + ], + "observed_change": "Codex adds an optional `role` field to `thread/realtime/appendText` so realtime text can be sent as `user` or `developer`, defaulting omitted roles to `user`.", + "public_signal_decision": "publish", + "publisher_angle": "operator_impact", + "repo": "openai/codex", + "schema": "upstream_impact/v1", + "slug": "openai-codex-pr-27936", + "social_notes": [ + "Frame the post around protocol-level authority for realtime app-provided context.", + "Make the backward-compatible default explicit.", + "Avoid implying that arbitrary user text should be upgraded to developer role." + ], + "source_refs": { + "items": [ + { + "kind": "pull_request", + "meta": "Merged 2026-06-12T22:05:38Z", + "title": "[codex] add roles to realtime append text", + "url": "https://github.com/openai/codex/pull/27936" + }, + { + "kind": "pull_request", + "meta": "artifacts/github/reviews/openai-codex-pr-27936.review.json", + "title": "Source-backed Decodex upstream review", + "url": "https://github.com/openai/codex/pull/27936" + } + ] + } +} diff --git a/artifacts/github/reviews/openai-codex-pr-27535.review.json b/artifacts/github/reviews/openai-codex-pr-27535.review.json new file mode 100644 index 000000000..3fb965805 --- /dev/null +++ b/artifacts/github/reviews/openai-codex-pr-27535.review.json @@ -0,0 +1,69 @@ +{ + "adoption_opportunity": "Prepare Decodex auth and MCP credential tooling to treat Codex encrypted local secrets as namespaced stores (`local.age`, `codex_auth.age`, and `mcp_oauth.age`) instead of one shared secrets file.", + "caveats": "PR #27535 is foundational plumbing only. Its PR body explicitly says it does not activate either auth backend or change existing credential behavior; activation arrives in follow-up PRs such as #27539 and the MCP OAuth stack.", + "changed_surfaces": [ + "config `AuthKeyringBackendKind` type", + "core config bootstrap auth-keyring resolution", + "core config schema feature flags", + "features crate `secret_auth_storage` feature", + "local secrets namespace selection", + "keyring store cloneability", + "auth-keyring and local-secrets tests" + ], + "community_value": "Medium. The change is useful context for upcoming encrypted auth storage behavior, especially on Windows, but it is not a standalone user-visible feature yet.", + "compatibility_risk": "Low for existing behavior because the PR does not activate the auth backend. Medium for future Decodex account tooling if it assumes all local encrypted secrets share one file or that CLI auth cannot move into a namespaced local-secrets file.", + "confidence": "confirmed", + "control_plane_relevance": "Medium. Decodex account and MCP credential management need to track Codex's move toward auth-specific encrypted secret namespaces before relying on file-only auth assumptions.", + "deprecated_or_breaking_notes": "No behavior is removed or activated by this PR. It adds the `secret_auth_storage` feature/config path and new namespace filenames for later auth and MCP OAuth storage.", + "evidence": [ + "PR #27535 states that CLI auth and MCP OAuth credentials should use separate encrypted files while sharing the existing local-secrets implementation, and also states that it does not activate either auth backend or change existing credential behavior.", + "`codex-rs/config/src/types.rs` adds `AuthKeyringBackendKind` with `Direct` and `Secrets` variants, defaulting to `Secrets` on Windows and `Direct` elsewhere.", + "`codex-rs/features/src/lib.rs` adds stable feature `secret_auth_storage`, default-enabled only on Windows.", + "`codex-rs/secrets/src/local.rs` adds `LocalSecretsNamespace` variants for managed secrets, Codex auth, and MCP OAuth, with filenames `local.age`, `codex_auth.age`, and `mcp_oauth.age`.", + "`codex-rs/secrets/src/lib.rs` adds namespaced `SecretsManager` construction.", + "`codex-rs/core/src/config/auth_keyring.rs` resolves bootstrap/full config auth keyring backend selection from the `secret_auth_storage` feature.", + "The normalized bundle `artifacts/github/bundles/openai-codex-pr-27535.json` records 11 changed files for the merged PR." + ], + "next_actions": [ + { + "reason": "The namespace and feature/config plumbing affects how later encrypted auth storage should be interpreted by Decodex account and MCP tooling.", + "type": "upstream_impact" + } + ], + "observed_change": "Codex adds auth-specific encrypted local-secrets namespaces and a `secret_auth_storage` config/feature path, without activating new credential storage behavior yet.", + "repo": "openai/codex", + "reviewed_at": "2026-06-15T06:07:50Z", + "schema": "upstream_review/v1", + "slug": "openai-codex-pr-27535", + "source_refs": { + "items": [ + { + "kind": "pull_request", + "meta": "Merged 2026-06-12T19:52:50Z", + "title": "feat: add auth-specific encrypted secret namespaces", + "url": "https://github.com/openai/codex/pull/27535" + }, + { + "kind": "commit", + "title": "Add secret auth storage config", + "url": "https://github.com/openai/codex/commit/07d66eccd904cd15cd754c43d3f74a72d63a1980" + }, + { + "kind": "commit", + "title": "Add auth-specific secret namespaces", + "url": "https://github.com/openai/codex/commit/e026ff2133ba7c2e740eee1ecf0cc0ec5a7479a8" + } + ] + }, + "subject": { + "commit_shas": [ + "07d66eccd904cd15cd754c43d3f74a72d63a1980", + "3e3379b56006b9e877f5db4ba65009b4ad41c49c", + "e026ff2133ba7c2e740eee1ecf0cc0ec5a7479a8", + "30f6722a49e562b8d8fccbaf221aa278d39ab30c" + ], + "subject_id": "27535", + "subject_kind": "pr" + }, + "user_visible_path": "No immediate user-visible path in this PR. It prepares the feature/config and local encrypted secret namespace model that later CLI auth and MCP OAuth storage use." +} diff --git a/artifacts/github/reviews/openai-codex-pr-27539.review.json b/artifacts/github/reviews/openai-codex-pr-27539.review.json new file mode 100644 index 000000000..6795bc0a7 --- /dev/null +++ b/artifacts/github/reviews/openai-codex-pr-27539.review.json @@ -0,0 +1,74 @@ +{ + "adoption_opportunity": "Update Decodex account switching, auth readback, app-server account handling, and account-pool diagnostics to use Codex auth APIs or auth-manager semantics instead of assuming `$CODEX_HOME/auth.json` is the only durable auth source.", + "caveats": "The behavior is selected only for keyring-mode auth when the resolved auth keyring backend is `Secrets`; PR #27535 makes that backend default on Windows through `secret_auth_storage`, while direct keyring storage remains available where selected.", + "changed_surfaces": [ + "CLI auth storage backend", + "login, logout, refresh, device-code login, and API-key login paths", + "TUI and exec bootstrap config loading", + "app-server account request processor", + "remote-control auth tests", + "cloud config and cloud task auth loading", + "auth storage integration tests" + ], + "community_value": "High for Windows and large ChatGPT-auth payload users because Codex can avoid Credential Manager blob-size limits by storing payloads in encrypted local secrets while keeping only the encryption key in the OS keyring.", + "compatibility_risk": "High. Tools that inspect, copy, switch, delete, or validate Codex auth by reading only `auth.json` can miss encrypted `codex_auth.age` auth state or stale fallback cleanup after this path is active.", + "confidence": "confirmed", + "control_plane_relevance": "High. Decodex App and Control Plane account management depend on reliable local auth switching, account refresh, logout, and diagnostics across CLI, TUI, and app-server paths.", + "deprecated_or_breaking_notes": "Successful encrypted saves remove stale `auth.json` fallback data, and logout removes encrypted, direct-keyring, and fallback auth data. File-only auth remains a mode, but keyring-mode auth can now be backed by encrypted local secrets.", + "evidence": [ + "PR #27539 says Windows Credential Manager limits generic credential blobs to 2,560 bytes and adds encrypted CLI-auth storage using the `CliAuth` secrets namespace.", + "The PR body says backend selection is applied consistently for login, logout, refresh, device-code login, auth loading, login restrictions, CLI, exec, TUI, app-server account handling, cloud config, and cloud tasks.", + "`codex-rs/login/src/auth/storage.rs` imports `LocalSecretsNamespace`, `SecretsManager`, `SecretScope`, and `SecretName`, and exports `AuthKeyringBackendKind` from config.", + "`codex-rs/login/src/auth/manager.rs` threads `AuthKeyringBackendKind` through `CodexAuth` and auth-manager construction.", + "`codex-rs/cli/src/login.rs` passes the resolved auth keyring backend through pre-login cleanup and login flows.", + "`codex-rs/app-server/src/request_processors/account_processor.rs` passes `config.auth_keyring_backend_kind()` when saving API-key auth and constructing login server options.", + "`codex-rs/login/src/auth/storage_tests.rs` adds coverage for direct and encrypted storage behavior.", + "The normalized bundle `artifacts/github/bundles/openai-codex-pr-27539.json` records 28 changed files for the merged PR." + ], + "next_actions": [ + { + "reason": "Encrypted CLI auth storage directly affects Decodex account switching, app-server account APIs, and local auth diagnostics.", + "type": "upstream_impact" + }, + { + "reason": "The change has a clear operator-facing public angle around large auth payloads and `auth.json` assumptions.", + "type": "social_candidate" + } + ], + "observed_change": "Codex activates encrypted local-secrets storage for CLI auth payloads when keyring-mode auth resolves to the `Secrets` backend.", + "repo": "openai/codex", + "reviewed_at": "2026-06-15T06:07:50Z", + "schema": "upstream_review/v1", + "slug": "openai-codex-pr-27539", + "source_refs": { + "items": [ + { + "kind": "pull_request", + "meta": "Merged 2026-06-12T21:23:50Z", + "title": "feat: use encrypted local secrets for CLI auth", + "url": "https://github.com/openai/codex/pull/27539" + }, + { + "kind": "commit", + "title": "Use encrypted local secrets for CLI auth", + "url": "https://github.com/openai/codex/commit/24a0cd4c2b9a14f672691335af172ce3462aeb79" + }, + { + "kind": "commit", + "title": "Fix device login auth cleanup backend", + "url": "https://github.com/openai/codex/commit/0dc3b430c878be6d9d5a7f19e72dd6567adc6933" + } + ] + }, + "subject": { + "commit_shas": [ + "24a0cd4c2b9a14f672691335af172ce3462aeb79", + "77343b8909e8a8ef9c6b65476c547f8681eb1554", + "d43079bc45170d8d8abf051718a446a592728416", + "0dc3b430c878be6d9d5a7f19e72dd6567adc6933" + ], + "subject_id": "27539", + "subject_kind": "pr" + }, + "user_visible_path": "Codex users on the encrypted keyring backend can keep large CLI auth payloads in encrypted local secrets rather than direct OS keyring blobs or only `auth.json`; login, logout, refresh, TUI, exec, and app-server account paths use the selected backend." +} diff --git a/artifacts/github/reviews/openai-codex-pr-27936.review.json b/artifacts/github/reviews/openai-codex-pr-27936.review.json new file mode 100644 index 000000000..76e5dc4de --- /dev/null +++ b/artifacts/github/reviews/openai-codex-pr-27936.review.json @@ -0,0 +1,65 @@ +{ + "adoption_opportunity": "Let Decodex app-server clients pass app-provided context, such as memory or durable operator context, through `thread/realtime/appendText` with developer authority instead of relying on renderer-owned side channels.", + "caveats": "The new `role` field is optional and defaults to `user`, so older JSON clients remain compatible. Integrations still need to be careful not to mark ordinary user text as developer-authority context.", + "changed_surfaces": [ + "app-server protocol JSON schemas", + "app-server protocol TypeScript exports", + "v2 realtime append-text params", + "app-server realtime request processor", + "core realtime conversation input queue", + "realtime websocket outbound conversation item creation", + "app-server README and protocol tests" + ], + "community_value": "High for app-server and Codex Apps integrators because it gives a protocol-level way to attach text as user or developer context while preserving backward compatibility.", + "compatibility_risk": "Medium. Existing JSON clients that omit `role` default to user, but typed clients and Control Plane integrations should update schemas and enforce authority boundaries for developer-role text.", + "confidence": "confirmed", + "control_plane_relevance": "High. Decodex Control Plane may append realtime text or app-provided context into Codex sessions and must preserve whether that text is user input or developer-authority context.", + "deprecated_or_breaking_notes": "No removal is shown. The field is added with `#[serde(default)]`, so omitted roles default to `user`.", + "evidence": [ + "PR #27936 says `thread/realtime/appendText` adds an explicit `user` or `developer` role and older JSON clients that omit the field continue to default to `user`.", + "`codex-rs/app-server-protocol/src/protocol/v2/realtime.rs` adds `role: ConversationTextRole` with `#[serde(default)]` to `ThreadRealtimeAppendTextParams`.", + "`codex-rs/protocol/src/protocol.rs` adds `ConversationTextRole` with `User` as the default and `Developer` as the second variant.", + "`codex-rs/app-server/src/request_processors/turn_processor.rs` passes `params.role` into `ConversationTextParams` for realtime text input.", + "`codex-rs/codex-api/src/endpoint/realtime_websocket/protocol.rs` changes outbound conversation item role serialization to use `ConversationTextRole`.", + "`codex-rs/app-server-protocol/src/protocol/v2/tests.rs` adds tests for defaulting to user and accepting developer role.", + "The normalized bundle `artifacts/github/bundles/openai-codex-pr-27936.json` records 18 changed files for the merged PR." + ], + "next_actions": [ + { + "reason": "The protocol addition affects Decodex app-server and realtime Control Plane clients.", + "type": "upstream_impact" + }, + { + "reason": "The change has a clear public integration angle around developer-authority context for realtime app-server text.", + "type": "social_candidate" + } + ], + "observed_change": "Codex adds an optional `role` field to `thread/realtime/appendText` so realtime text can be sent as `user` or `developer`, defaulting omitted roles to `user`.", + "repo": "openai/codex", + "reviewed_at": "2026-06-15T06:07:50Z", + "schema": "upstream_review/v1", + "slug": "openai-codex-pr-27936", + "source_refs": { + "items": [ + { + "kind": "pull_request", + "meta": "Merged 2026-06-12T22:05:38Z", + "title": "[codex] add roles to realtime append text", + "url": "https://github.com/openai/codex/pull/27936" + }, + { + "kind": "commit", + "title": "add roles to realtime append text", + "url": "https://github.com/openai/codex/commit/728db43093fb3390778de642164be5520aa9262a" + } + ] + }, + "subject": { + "commit_shas": [ + "728db43093fb3390778de642164be5520aa9262a" + ], + "subject_id": "27936", + "subject_kind": "pr" + }, + "user_visible_path": "App-server clients can call `thread/realtime/appendText` with `role: \"developer\"` for app-provided context, while existing clients that omit `role` continue to send user-role text." +} diff --git a/artifacts/github/social-candidates/openai-codex-pr-27539.json b/artifacts/github/social-candidates/openai-codex-pr-27539.json new file mode 100644 index 000000000..c49d36443 --- /dev/null +++ b/artifacts/github/social-candidates/openai-codex-pr-27539.json @@ -0,0 +1,54 @@ +{ + "audience": "Control Plane operators and Codex account tooling maintainers", + "candidate_text": [ + "Codex CLI auth now has an encrypted local-secrets backend for large ChatGPT auth payloads, selected through `secret_auth_storage`. Control-plane account tools should stop assuming auth.json is the only auth source. PR: https://github.com/openai/codex/pull/27539" + ], + "caveats": [ + "Do not imply direct keyring storage is removed.", + "Keep the claim scoped to keyring-mode CLI auth that resolves to the encrypted local-secrets backend.", + "Do not claim MCP OAuth storage is activated by this PR." + ], + "channel": "x", + "claims": [ + { + "confidence": "confirmed", + "evidence": "artifacts/github/reviews/openai-codex-pr-27539.review.json", + "text": "Codex can store CLI auth payloads in encrypted local secrets when the selected keyring backend is `Secrets`." + }, + { + "confidence": "confirmed", + "evidence": "artifacts/github/impact/openai-codex-pr-27539.json", + "text": "File-only `auth.json` assumptions are a Control Plane compatibility risk." + } + ], + "decision": { + "idempotency_key": "x:decodexspace:openai-codex-pr-27539:operator_impact", + "reason": "The change has a concrete account-tooling compatibility angle with direct source links.", + "worthiness": "publish" + }, + "evidence_notes": [ + "PR #27539 activates encrypted local-secrets storage for CLI auth on the selected backend.", + "The app-server account processor passes `config.auth_keyring_backend_kind()` through account save/login paths.", + "The PR removes stale `auth.json` fallback data after encrypted saves and removes all auth storage variants during logout." + ], + "mode": "operator_impact", + "next_steps": [ + "Let Publisher automation decide whether to reserve or publish this candidate." + ], + "priority": "high", + "repo": "openai/codex", + "schema": "social_candidate/v1", + "slug": "openai-codex-pr-27539", + "source_refs": { + "upstream_impacts": [ + "artifacts/github/impact/openai-codex-pr-27539.json" + ], + "upstream_reviews": [ + "artifacts/github/reviews/openai-codex-pr-27539.review.json" + ], + "urls": [ + "https://github.com/openai/codex/pull/27539" + ] + }, + "target_account": "decodexspace" +} diff --git a/artifacts/github/social-candidates/openai-codex-pr-27936.json b/artifacts/github/social-candidates/openai-codex-pr-27936.json new file mode 100644 index 000000000..fce82cb16 --- /dev/null +++ b/artifacts/github/social-candidates/openai-codex-pr-27936.json @@ -0,0 +1,54 @@ +{ + "audience": "App-server integrators and Control Plane operators", + "candidate_text": [ + "Codex app-server `thread/realtime/appendText` now accepts `role: \"user\" | \"developer\"` and defaults older clients to user. That gives apps a protocol path for developer-authority context like memory. PR: https://github.com/openai/codex/pull/27936" + ], + "caveats": [ + "Do not imply older JSON clients break; omitted role defaults to user.", + "Do not suggest user-authored text should be sent as developer authority.", + "Keep the claim scoped to realtime appendText and app-server protocol clients." + ], + "channel": "x", + "claims": [ + { + "confidence": "confirmed", + "evidence": "artifacts/github/reviews/openai-codex-pr-27936.review.json", + "text": "`thread/realtime/appendText` now accepts user or developer role text." + }, + { + "confidence": "confirmed", + "evidence": "artifacts/github/impact/openai-codex-pr-27936.json", + "text": "The optional role field is an adoption opportunity for app-server and Control Plane realtime clients." + } + ], + "decision": { + "idempotency_key": "x:decodexspace:openai-codex-pr-27936:operator_impact", + "reason": "The change has a concrete protocol and authority-semantics angle with direct source links.", + "worthiness": "publish" + }, + "evidence_notes": [ + "The protocol adds `ConversationTextRole` with `user` and `developer` variants.", + "`ThreadRealtimeAppendTextParams` defaults omitted roles to user.", + "The app-server request processor and realtime websocket writer propagate the role into conversation item creation." + ], + "mode": "operator_impact", + "next_steps": [ + "Let Publisher automation decide whether to reserve or publish this candidate." + ], + "priority": "high", + "repo": "openai/codex", + "schema": "social_candidate/v1", + "slug": "openai-codex-pr-27936", + "source_refs": { + "upstream_impacts": [ + "artifacts/github/impact/openai-codex-pr-27936.json" + ], + "upstream_reviews": [ + "artifacts/github/reviews/openai-codex-pr-27936.review.json" + ], + "urls": [ + "https://github.com/openai/codex/pull/27936" + ] + }, + "target_account": "decodexspace" +}