diff --git a/Cargo.lock b/Cargo.lock index 181fe82ecfd1..6aebf48de709 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3050,6 +3050,7 @@ dependencies = [ "urlencoding", "utoipa", "uuid", + "v_htmlescape", "webbrowser", "which 8.0.0", "winapi", @@ -8584,6 +8585,12 @@ dependencies = [ "vsimd", ] +[[package]] +name = "v_htmlescape" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" + [[package]] name = "valuable" version = "0.1.1" diff --git a/clippy-baselines/too_many_lines.txt b/clippy-baselines/too_many_lines.txt index 5e58ce86c936..ae7ca874e58c 100644 --- a/clippy-baselines/too_many_lines.txt +++ b/clippy-baselines/too_many_lines.txt @@ -1,17 +1,10 @@ -crates/goose-bench/src/eval_suites/core/developer/simple_repo_clone_test.rs::run -crates/goose-cli/src/cli.rs::cli -crates/goose-cli/src/commands/configure.rs::configure_extensions_dialog crates/goose-cli/src/commands/configure.rs::configure_provider_dialog crates/goose-cli/src/commands/configure.rs::configure_tool_permissions_dialog -crates/goose-cli/src/commands/configure.rs::handle_configure crates/goose-cli/src/commands/project.rs::handle_project_default crates/goose-cli/src/commands/project.rs::handle_projects_interactive -crates/goose-cli/src/commands/web.rs::process_message_streaming crates/goose-cli/src/session/builder.rs::build_session crates/goose-cli/src/session/export.rs::tool_response_to_markdown -crates/goose-cli/src/session/mod.rs::interactive crates/goose-cli/src/session/mod.rs::process_agent_response -crates/goose-mcp/src/computercontroller/docx_tool.rs::docx_tool crates/goose-mcp/src/computercontroller/mod.rs::new crates/goose-mcp/src/computercontroller/pdf_tool.rs::pdf_tool crates/goose-mcp/src/memory/mod.rs::new @@ -23,12 +16,10 @@ crates/goose/src/agents/agent.rs::create_recipe crates/goose/src/agents/agent.rs::dispatch_tool_call crates/goose/src/agents/agent.rs::reply crates/goose/src/agents/agent.rs::reply_internal -crates/goose/src/agents/extension_manager.rs::add_extension crates/goose/src/providers/formats/anthropic.rs::format_messages -crates/goose/src/providers/formats/anthropic.rs::response_to_streaming_message +crates/goose/src/providers/formats/anthropic.rs::response_to_streaming_message crates/goose/src/providers/formats/databricks.rs::format_messages crates/goose/src/providers/formats/google.rs::format_messages crates/goose/src/providers/formats/openai.rs::format_messages -crates/goose/src/providers/formats/openai.rs::response_to_streaming_message -crates/goose/src/providers/gcpvertexai.rs::post_with_location +crates/goose/src/providers/formats/openai.rs::response_to_streaming_message crates/goose/src/providers/snowflake.rs::post diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index be4634b17de2..8874fa41db69 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -352,6 +352,7 @@ derive_utoipa!(Icon as IconSchema); super::routes::config_management::remove_custom_provider, super::routes::config_management::check_provider, super::routes::config_management::set_config_provider, + super::routes::config_management::configure_provider_oauth, super::routes::config_management::get_pricing, super::routes::prompts::get_prompts, super::routes::prompts::get_prompt, diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index 05b0d013269c..3dbc23d4f8fe 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -210,6 +210,13 @@ fn mask_secret(secret: Value) -> String { format!("{}{}", visible, mask) } +fn is_valid_provider_name(provider_name: &str) -> bool { + !provider_name.is_empty() + && provider_name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') +} + #[utoipa::path( post, path = "/config/read", @@ -823,6 +830,54 @@ pub async fn set_config_provider( Ok(()) } +#[utoipa::path( + post, + path = "/config/providers/{name}/oauth", + params( + ("name" = String, Path, description = "Provider name") + ), + responses( + (status = 200, description = "OAuth configuration completed"), + (status = 400, description = "OAuth configuration failed") + ) +)] +pub async fn configure_provider_oauth( + Path(provider_name): Path, +) -> Result, (StatusCode, String)> { + use goose::model::ModelConfig; + use goose::providers::create; + + if !is_valid_provider_name(&provider_name) { + return Err((StatusCode::BAD_REQUEST, "Invalid provider name".to_string())); + } + + let temp_model = + ModelConfig::new("temp").map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?; + + let provider = create(&provider_name, temp_model).await.map_err(|e| { + ( + StatusCode::BAD_REQUEST, + format!("Failed to create provider: {}", e), + ) + })?; + + provider.configure_oauth().await.map_err(|e| { + ( + StatusCode::BAD_REQUEST, + format!("OAuth configuration failed: {}", e), + ) + })?; + + // Mark the provider as configured after successful OAuth + let configured_marker = format!("{}_configured", provider_name); + let config = goose::config::Config::global(); + config + .set_param(&configured_marker, true) + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + Ok(Json("OAuth configuration completed".to_string())) +} + pub fn routes(state: Arc) -> Router { Router::new() .route("/config", get(read_all_config)) @@ -851,6 +906,10 @@ pub fn routes(state: Arc) -> Router { .route("/config/custom-providers/{id}", get(get_custom_provider)) .route("/config/check_provider", post(check_provider)) .route("/config/set_provider", post(set_config_provider)) + .route( + "/config/providers/{name}/oauth", + post(configure_provider_oauth), + ) .with_state(state) } diff --git a/crates/goose-server/src/routes/utils.rs b/crates/goose-server/src/routes/utils.rs index 9d5e19957d9d..78a78151d893 100644 --- a/crates/goose-server/src/routes/utils.rs +++ b/crates/goose-server/src/routes/utils.rs @@ -101,6 +101,16 @@ pub fn check_provider_configured(metadata: &ProviderMetadata, provider_type: Pro .is_ok(); } } + + // Special case: OAuth providers - check for configured marker + let has_oauth_key = metadata.config_keys.iter().any(|key| key.oauth_flow); + if has_oauth_key { + let configured_marker = format!("{}_configured", metadata.name); + if matches!(config.get_param::(&configured_marker), Ok(true)) { + return true; + } + } + // Special case: Zero-config providers (no config keys) if metadata.config_keys.is_empty() { // Check if the provider has been explicitly configured via the UI diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index 4758621ded71..e76a8fe15404 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -77,6 +77,7 @@ rand = "0.8.5" utoipa = { version = "4.1", features = ["chrono"] } tokio-cron-scheduler = "0.14.0" urlencoding = "2.1" +v_htmlescape = "0.15" sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "chrono", "json"] } # For Bedrock provider diff --git a/crates/goose/src/providers/canonical/data/canonical_mapping_report.json b/crates/goose/src/providers/canonical/data/canonical_mapping_report.json index d4323ac233e0..ddacdcf0b14a 100644 --- a/crates/goose/src/providers/canonical/data/canonical_mapping_report.json +++ b/crates/goose/src/providers/canonical/data/canonical_mapping_report.json @@ -3024,6 +3024,10 @@ "provider_model": "gpt-5.1-codex-mini", "canonical_model": "openai/gpt-5.1-codex-mini" }, + { + "provider_model": "gpt-5.2-codex", + "canonical_model": "openai/gpt-5.2-codex" + }, { "provider_model": "gpt-5.2", "canonical_model": "openai/gpt-5.2" diff --git a/crates/goose/src/providers/canonical/data/canonical_models.json b/crates/goose/src/providers/canonical/data/canonical_models.json index abc79e4f8b52..8eddcd0dd8f0 100644 --- a/crates/goose/src/providers/canonical/data/canonical_models.json +++ b/crates/goose/src/providers/canonical/data/canonical_models.json @@ -2181,6 +2181,27 @@ "image": 0.0 } }, + { + "id": "openai/gpt-5.2-codex", + "name": "OpenAI: GPT-5.2-Codex", + "context_length": 400000, + "max_completion_tokens": 128000, + "input_modalities": [ + "file", + "image", + "text" + ], + "output_modalities": [ + "text" + ], + "supports_tools": true, + "pricing": { + "prompt": 1.75e-6, + "completion": 0.000014, + "request": 0.0, + "image": 0.0 + } + }, { "id": "openai/gpt-5.2-pro", "name": "OpenAI: GPT-5.2 Pro", diff --git a/crates/goose/src/providers/canonical/name_builder.rs b/crates/goose/src/providers/canonical/name_builder.rs index 5765cfcbd82a..03953fcea3e2 100644 --- a/crates/goose/src/providers/canonical/name_builder.rs +++ b/crates/goose/src/providers/canonical/name_builder.rs @@ -133,7 +133,10 @@ fn swap_claude_word_order(model: &str) -> Option { } fn is_hosting_provider(provider: &str) -> bool { - matches!(provider, "databricks" | "openrouter" | "azure" | "bedrock") + matches!( + provider, + "databricks" | "openrouter" | "azure" | "bedrock" | "chatgpt_codex" + ) } /// Infer the real provider from model name patterns diff --git a/crates/goose/src/providers/chatgpt_codex.rs b/crates/goose/src/providers/chatgpt_codex.rs new file mode 100644 index 000000000000..0a96dc9fb35a --- /dev/null +++ b/crates/goose/src/providers/chatgpt_codex.rs @@ -0,0 +1,1234 @@ +use crate::config::paths::Paths; +use crate::conversation::message::{Message, MessageContent}; +use crate::model::ModelConfig; +use crate::providers::api_client::AuthProvider; +use crate::providers::base::{ConfigKey, MessageStream, Provider, ProviderMetadata, ProviderUsage}; +use crate::providers::errors::ProviderError; +use crate::providers::formats::openai_responses::responses_api_to_streaming_message; +use crate::providers::retry::ProviderRetry; +use crate::providers::utils::handle_status_openai_compat; +use anyhow::{anyhow, Result}; +use async_stream::try_stream; +use async_trait::async_trait; +use axum::{extract::Query, response::Html, routing::get, Router}; +use base64::Engine; +use chrono::{DateTime, Utc}; +use futures::{StreamExt, TryStreamExt}; +use jsonwebtoken::jwk::JwkSet; +use jsonwebtoken::{decode, decode_header, DecodingKey, Validation}; +use rmcp::model::{RawContent, Role, Tool}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use sha2::Digest; +use std::io; +use std::net::SocketAddr; +use std::ops::Deref; +use std::path::PathBuf; +use std::sync::{Arc, LazyLock}; +use tokio::pin; +use tokio::sync::{oneshot, Mutex as TokioMutex}; +use tokio_util::codec::{FramedRead, LinesCodec}; +use tokio_util::io::StreamReader; + +const CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann"; +const ISSUER: &str = "https://auth.openai.com"; +const CODEX_API_ENDPOINT: &str = "https://chatgpt.com/backend-api/codex"; +const OAUTH_SCOPES: &[&str] = &["openid", "profile", "email", "offline_access"]; +// Canonical localhost callback port for Codex OAuth (default localhost:1455 per OpenAI docs). +// https://developers.openai.com/codex/auth/ +const OAUTH_PORT: u16 = 1455; +// Allow time for users to complete the browser-based OAuth flow. +const OAUTH_TIMEOUT_SECS: u64 = 300; +const HTML_AUTO_CLOSE_TIMEOUT_MS: u64 = 2000; + +pub const CHATGPT_CODEX_DEFAULT_MODEL: &str = "gpt-5.1-codex"; +pub const CHATGPT_CODEX_KNOWN_MODELS: &[&str] = &[ + "gpt-5.2-codex", + "gpt-5.1-codex", + "gpt-5.1-codex-mini", + "gpt-5.1-codex-max", +]; + +const CHATGPT_CODEX_DOC_URL: &str = "https://openai.com/chatgpt"; + +#[derive(Debug)] +struct ChatGptCodexAuthState { + oauth_mutex: TokioMutex<()>, + jwks_cache: TokioMutex>, +} + +impl ChatGptCodexAuthState { + fn new() -> Self { + Self { + oauth_mutex: TokioMutex::new(()), + jwks_cache: TokioMutex::new(None), + } + } + + fn instance() -> Arc { + Arc::clone(&CHATGPT_CODEX_AUTH_STATE) + } +} + +static CHATGPT_CODEX_AUTH_STATE: LazyLock> = + LazyLock::new(|| Arc::new(ChatGptCodexAuthState::new())); + +fn build_input_items(messages: &[Message]) -> Result> { + let mut items = Vec::new(); + + for message in messages.iter().filter(|m| m.is_agent_visible()) { + let role = match message.role { + Role::User => Some("user"), + Role::Assistant => Some("assistant"), + }; + let mut content_items: Vec = Vec::new(); + + let flush_text = |items: &mut Vec, role: Option<&str>, content: &mut Vec| { + if let Some(role) = role { + if !content.is_empty() { + items.push(json!({ "role": role, "content": std::mem::take(content) })); + } + } else { + content.clear(); + } + }; + + for content in &message.content { + match content { + MessageContent::Text(text) => { + if !text.text.is_empty() { + let content_type = if message.role == Role::Assistant { + "output_text" + } else { + "input_text" + }; + content_items.push(json!({ "type": content_type, "text": text.text })); + } + } + MessageContent::ToolRequest(request) => { + flush_text(&mut items, role, &mut content_items); + if let Ok(tool_call) = &request.tool_call { + let arguments_str = match tool_call.arguments.as_ref() { + Some(args) => serde_json::to_string(args)?, + None => "{}".to_string(), + }; + items.push(json!({ + "type": "function_call", + "call_id": request.id, + "name": tool_call.name, + "arguments": arguments_str + })); + } + } + MessageContent::ToolResponse(response) => { + flush_text(&mut items, role, &mut content_items); + match &response.tool_result { + Ok(contents) => { + let text_content: Vec = contents + .content + .iter() + .filter_map(|c| { + if let RawContent::Text(t) = c.deref() { + Some(t.text.clone()) + } else { + None + } + }) + .collect(); + if !text_content.is_empty() { + items.push(json!({ + "type": "function_call_output", + "call_id": response.id, + "output": text_content.join("\n") + })); + } + } + Err(error_data) => { + items.push(json!({ + "type": "function_call_output", + "call_id": response.id, + "output": format!("Error: {}", error_data.message) + })); + } + } + } + _ => {} + } + } + + flush_text(&mut items, role, &mut content_items); + } + + Ok(items) +} + +fn create_codex_request( + model_config: &ModelConfig, + system: &str, + messages: &[Message], + tools: &[Tool], +) -> Result { + let input_items = build_input_items(messages)?; + + let mut payload = json!({ + "model": model_config.model_name, + "input": input_items, + "store": false, + "instructions": system, + }); + + let payload_obj = payload + .as_object_mut() + .ok_or_else(|| anyhow!("Codex payload must be a JSON object"))?; + + if !tools.is_empty() { + let tools_spec: Vec = tools + .iter() + .map(|tool| { + json!({ + "type": "function", + "name": tool.name, + "description": tool.description, + "parameters": tool.input_schema, + }) + }) + .collect(); + + payload_obj.insert("tools".to_string(), json!(tools_spec)); + } + + if let Some(temp) = model_config.temperature { + payload_obj.insert("temperature".to_string(), json!(temp)); + } + + Ok(payload) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct TokenData { + access_token: String, + refresh_token: String, + id_token: Option, + expires_at: DateTime, + account_id: Option, +} + +#[derive(Debug, Clone)] +struct TokenCache { + cache_path: PathBuf, +} + +fn get_cache_path() -> PathBuf { + Paths::in_config_dir("chatgpt_codex/tokens.json") +} + +impl TokenCache { + fn new() -> Self { + let cache_path = get_cache_path(); + if let Some(parent) = cache_path.parent() { + let _ = std::fs::create_dir_all(parent); + } + Self { cache_path } + } + + fn load(&self) -> Option { + if let Ok(contents) = std::fs::read_to_string(&self.cache_path) { + serde_json::from_str(&contents).ok() + } else { + None + } + } + + fn save(&self, token_data: &TokenData) -> Result<()> { + if let Some(parent) = self.cache_path.parent() { + std::fs::create_dir_all(parent)?; + } + let contents = serde_json::to_string(token_data)?; + std::fs::write(&self.cache_path, contents)?; + Ok(()) + } + + fn clear(&self) { + let _ = std::fs::remove_file(&self.cache_path); + } +} + +#[derive(Debug, Deserialize)] +struct JwtClaims { + chatgpt_account_id: Option, + #[serde(rename = "https://api.openai.com/auth")] + auth_claims: Option, + organizations: Option>, +} + +#[derive(Debug, Deserialize)] +struct AuthClaims { + chatgpt_account_id: Option, +} + +#[derive(Debug, Deserialize)] +struct OrgInfo { + id: String, +} + +#[derive(Debug, Deserialize)] +struct OidcConfiguration { + jwks_uri: String, +} + +async fn fetch_jwks_for(issuer: &str) -> Result { + let client = reqwest::Client::new(); + let config_url = format!("{}/.well-known/openid-configuration", issuer); + let config = client + .get(config_url) + .send() + .await? + .error_for_status()? + .json::() + .await?; + + let jwks = client + .get(config.jwks_uri) + .send() + .await? + .error_for_status()? + .json::() + .await?; + + Ok(jwks) +} + +async fn get_jwks(state: &ChatGptCodexAuthState) -> Result { + let mut cache = state.jwks_cache.lock().await; + if let Some(jwks) = cache.clone() { + return Ok(jwks); + } + let jwks = fetch_jwks_for(ISSUER).await?; + *cache = Some(jwks.clone()); + Ok(jwks) +} + +fn parse_jwt_claims_with_jwks(token: &str, jwks: &JwkSet) -> Result { + let header = decode_header(token)?; + let kid = header + .kid + .ok_or_else(|| anyhow!("JWT header missing kid"))?; + let jwk = jwks + .find(&kid) + .ok_or_else(|| anyhow!("JWT signing key not found"))?; + let decoding_key = DecodingKey::from_jwk(jwk)?; + + let mut validation = Validation::new(header.alg); + validation.validate_aud = false; + + let token_data = decode::(token, &decoding_key, &validation)?; + Ok(token_data.claims) +} + +fn parse_jwt_claims_unverified(token: &str) -> Option { + let parts: Vec<&str> = token.split('.').collect(); + if parts.len() != 3 { + return None; + } + let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(parts[1]) + .ok()?; + serde_json::from_slice(&payload).ok() +} + +async fn parse_jwt_claims(token: &str, state: &ChatGptCodexAuthState) -> Option { + if let Ok(jwks) = get_jwks(state).await { + if let Ok(claims) = parse_jwt_claims_with_jwks(token, &jwks) { + return Some(claims); + } + } + parse_jwt_claims_unverified(token) +} + +fn account_id_from_claims(claims: &JwtClaims) -> Option { + if let Some(id) = claims.chatgpt_account_id.as_ref() { + return Some(id.clone()); + } + if let Some(auth) = claims.auth_claims.as_ref() { + if let Some(id) = auth.chatgpt_account_id.as_ref() { + return Some(id.clone()); + } + } + if let Some(orgs) = claims.organizations.as_ref() { + if let Some(org) = orgs.first() { + return Some(org.id.clone()); + } + } + None +} + +async fn extract_account_id( + token_data: &TokenData, + state: &ChatGptCodexAuthState, +) -> Option { + if let Some(id_token) = token_data.id_token.as_deref() { + if let Some(claims) = parse_jwt_claims(id_token, state).await { + if let Some(account_id) = account_id_from_claims(&claims) { + return Some(account_id); + } + } + } + + parse_jwt_claims(&token_data.access_token, state) + .await + .and_then(|claims| account_id_from_claims(&claims)) +} + +struct PkceChallenge { + verifier: String, + challenge: String, +} + +fn generate_pkce() -> PkceChallenge { + let verifier = nanoid::nanoid!(43); + let digest = sha2::Sha256::digest(verifier.as_bytes()); + let challenge = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest); + PkceChallenge { + verifier, + challenge, + } +} + +fn generate_state() -> String { + nanoid::nanoid!(32) +} + +fn build_authorize_url(redirect_uri: &str, pkce: &PkceChallenge, state: &str) -> Result { + let scopes = OAUTH_SCOPES.join(" "); + let params = [ + ("response_type", "code"), + ("client_id", CLIENT_ID), + ("redirect_uri", redirect_uri), + ("scope", &scopes), + ("code_challenge", &pkce.challenge), + ("code_challenge_method", "S256"), + ("id_token_add_organizations", "true"), + ("codex_cli_simplified_flow", "true"), + ("state", state), + ("originator", "goose"), + ]; + let query = serde_urlencoded::to_string(params)?; + Ok(format!("{}/oauth/authorize?{}", ISSUER, query)) +} + +#[derive(Debug, Deserialize)] +struct TokenResponse { + access_token: String, + refresh_token: String, + id_token: Option, + expires_in: Option, +} + +async fn exchange_code_for_tokens_with_issuer( + issuer: &str, + code: &str, + redirect_uri: &str, + pkce: &PkceChallenge, +) -> Result { + let client = reqwest::Client::new(); + let params = [ + ("grant_type", "authorization_code"), + ("code", code), + ("redirect_uri", redirect_uri), + ("client_id", CLIENT_ID), + ("code_verifier", &pkce.verifier), + ]; + + let resp = client + .post(format!("{}/oauth/token", issuer)) + .header("Content-Type", "application/x-www-form-urlencoded") + .form(¶ms) + .send() + .await?; + + if !resp.status().is_success() { + let status = resp.status(); + let text = resp.text().await.unwrap_or_default(); + return Err(anyhow!("Token exchange failed ({}): {}", status, text)); + } + + Ok(resp.json().await?) +} + +async fn refresh_access_token_with_issuer( + issuer: &str, + refresh_token: &str, +) -> Result { + let client = reqwest::Client::new(); + let params = [ + ("grant_type", "refresh_token"), + ("refresh_token", refresh_token), + ("client_id", CLIENT_ID), + ]; + + let resp = client + .post(format!("{}/oauth/token", issuer)) + .header("Content-Type", "application/x-www-form-urlencoded") + .form(¶ms) + .send() + .await?; + + if !resp.status().is_success() { + let status = resp.status(); + let text = resp.text().await.unwrap_or_default(); + return Err(anyhow!("Token refresh failed ({}): {}", status, text)); + } + + Ok(resp.json().await?) +} + +const HTML_SUCCESS_TEMPLATE: &str = r#" + + + goose - ChatGPT Authorization Successful + + + +
+

Authorization Successful

+

You can close this window and return to goose.

+
+ + +"#; + +fn html_success() -> String { + HTML_SUCCESS_TEMPLATE.replace( + "__AUTO_CLOSE_TIMEOUT_MS__", + &HTML_AUTO_CLOSE_TIMEOUT_MS.to_string(), + ) +} + +fn html_error(error: &str) -> String { + let safe_error = v_htmlescape::escape(error).to_string(); + format!( + r#" + + + goose - ChatGPT Authorization Failed + + + +
+

Authorization Failed

+

An error occurred during authorization.

+
{}
+
+ +"#, + safe_error + ) +} + +#[derive(Deserialize)] +struct CallbackParams { + code: Option, + state: Option, + error: Option, + error_description: Option, +} + +fn oauth_callback_router( + expected_state: String, + tx: Arc>>>>, +) -> Router { + Router::new().route( + "/auth/callback", + get(move |Query(params): Query| { + let tx = tx.clone(); + let expected = expected_state.clone(); + async move { + if let Some(error) = params.error { + let msg = params.error_description.unwrap_or(error); + if let Some(sender) = tx.lock().await.take() { + let _ = sender.send(Err(anyhow!("{}", msg))); + } + return Html(html_error(&msg)); + } + + let code = match params.code { + Some(c) => c, + None => { + let msg = "Missing authorization code"; + if let Some(sender) = tx.lock().await.take() { + let _ = sender.send(Err(anyhow!("{}", msg))); + } + return Html(html_error(msg)); + } + }; + + if params.state.as_deref() != Some(&expected) { + let msg = "Invalid state - potential CSRF attack"; + if let Some(sender) = tx.lock().await.take() { + let _ = sender.send(Err(anyhow!("{}", msg))); + } + return Html(html_error(msg)); + } + + if let Some(sender) = tx.lock().await.take() { + let _ = sender.send(Ok(code)); + } + Html(html_success()) + } + }), + ) +} + +async fn spawn_oauth_server(app: Router) -> Result> { + let addr = SocketAddr::from(([127, 0, 0, 1], OAUTH_PORT)); + let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| { + if e.kind() == io::ErrorKind::AddrInUse { + anyhow!( + "OAuth callback server failed to bind to {}: port {} is already in use. \ + Please stop the process using this port and try again.", + addr, + OAUTH_PORT + ) + } else { + anyhow!("OAuth callback server failed to bind to {}: {}", addr, e) + } + })?; + Ok(tokio::spawn(async move { + let server = axum::serve(listener, app); + let _ = server.await; + })) +} + +struct ServerHandleGuard(Option>); + +impl ServerHandleGuard { + fn new(handle: tokio::task::JoinHandle<()>) -> Self { + Self(Some(handle)) + } + + fn abort(&mut self) { + if let Some(handle) = self.0.take() { + handle.abort(); + } + } +} + +impl Drop for ServerHandleGuard { + fn drop(&mut self) { + self.abort(); + } +} + +async fn wait_for_oauth_code(rx: oneshot::Receiver>) -> Result { + let code_result = + tokio::time::timeout(std::time::Duration::from_secs(OAUTH_TIMEOUT_SECS), rx).await; + code_result + .map_err(|_| anyhow!("OAuth flow timed out"))?? + .map_err(|e| anyhow!("OAuth callback error: {}", e)) +} + +async fn perform_oauth_flow(auth_state: &ChatGptCodexAuthState) -> Result { + let _guard = auth_state.oauth_mutex.try_lock().map_err(|_| { + anyhow!("Another OAuth flow is already in progress; please try again later") + })?; + + let pkce = generate_pkce(); + let csrf_state = generate_state(); + let redirect_uri = format!("http://localhost:{}/auth/callback", OAUTH_PORT); + let auth_url = build_authorize_url(&redirect_uri, &pkce, &csrf_state)?; + + let (tx, rx) = oneshot::channel::>(); + let tx = Arc::new(TokioMutex::new(Some(tx))); + let app = oauth_callback_router(csrf_state.clone(), tx); + let server_handle = spawn_oauth_server(app).await?; + let mut server_guard = ServerHandleGuard::new(server_handle); + + if webbrowser::open(&auth_url).is_err() { + tracing::info!("Please open this URL in your browser:\n{}", auth_url); + } + + let code_result = wait_for_oauth_code(rx).await; + server_guard.abort(); + let code = code_result?; + + let tokens = exchange_code_for_tokens_with_issuer(ISSUER, &code, &redirect_uri, &pkce).await?; + + let expires_at = Utc::now() + chrono::Duration::seconds(tokens.expires_in.unwrap_or(3600)); + + let mut token_data = TokenData { + access_token: tokens.access_token, + refresh_token: tokens.refresh_token, + id_token: tokens.id_token, + expires_at, + account_id: None, + }; + + token_data.account_id = extract_account_id(&token_data, auth_state).await; + + Ok(token_data) +} + +#[derive(Debug)] +struct ChatGptCodexAuthProvider { + cache: TokenCache, + state: Arc, +} + +impl ChatGptCodexAuthProvider { + fn new(state: Arc) -> Self { + Self { + cache: TokenCache::new(), + state, + } + } + + async fn get_valid_token(&self) -> Result { + if let Some(mut token_data) = self.cache.load() { + if token_data.expires_at > Utc::now() + chrono::Duration::seconds(60) { + return Ok(token_data); + } + + tracing::debug!("Token expired, attempting refresh"); + match refresh_access_token_with_issuer(ISSUER, &token_data.refresh_token).await { + Ok(new_tokens) => { + token_data.access_token = new_tokens.access_token; + token_data.refresh_token = new_tokens.refresh_token; + if new_tokens.id_token.is_some() { + token_data.id_token = new_tokens.id_token; + } + token_data.expires_at = Utc::now() + + chrono::Duration::seconds(new_tokens.expires_in.unwrap_or(3600)); + if token_data.account_id.is_none() { + token_data.account_id = + extract_account_id(&token_data, self.state.as_ref()).await; + } + self.cache.save(&token_data)?; + tracing::info!("Token refreshed successfully"); + return Ok(token_data); + } + Err(e) => { + tracing::warn!("Token refresh failed, will re-authenticate: {}", e); + self.cache.clear(); + } + } + } + + tracing::info!("Starting OAuth flow for ChatGPT Codex"); + let token_data = perform_oauth_flow(self.state.as_ref()).await?; + self.cache.save(&token_data)?; + Ok(token_data) + } +} + +#[async_trait] +impl AuthProvider for ChatGptCodexAuthProvider { + async fn get_auth_header(&self) -> Result<(String, String)> { + let token_data = self.get_valid_token().await?; + Ok(( + "Authorization".to_string(), + format!("Bearer {}", token_data.access_token), + )) + } +} + +#[derive(Debug, serde::Serialize)] +pub struct ChatGptCodexProvider { + #[serde(skip)] + auth_provider: Arc, + model: ModelConfig, + #[serde(skip)] + name: String, +} + +impl ChatGptCodexProvider { + pub async fn from_env(model: ModelConfig) -> Result { + let auth_provider = Arc::new(ChatGptCodexAuthProvider::new( + ChatGptCodexAuthState::instance(), + )); + + Ok(Self { + auth_provider, + model, + name: Self::metadata().name, + }) + } + + async fn post_streaming(&self, payload: &Value) -> Result { + let token_data = self + .auth_provider + .get_valid_token() + .await + .map_err(|e| ProviderError::Authentication(e.to_string()))?; + + let mut headers = reqwest::header::HeaderMap::new(); + if let Some(account_id) = &token_data.account_id { + headers.insert( + reqwest::header::HeaderName::from_static("chatgpt-account-id"), + reqwest::header::HeaderValue::from_str(account_id) + .map_err(|e| ProviderError::ExecutionError(e.to_string()))?, + ); + } + + let client = reqwest::Client::new(); + let response = client + .post(format!("{}/responses", CODEX_API_ENDPOINT)) + .header( + "Authorization", + format!("Bearer {}", token_data.access_token), + ) + .header("Content-Type", "application/json") + .headers(headers) + .json(payload) + .send() + .await + .map_err(|e| ProviderError::RequestFailed(e.to_string()))?; + + handle_status_openai_compat(response).await + } +} + +#[async_trait] +impl Provider for ChatGptCodexProvider { + fn metadata() -> ProviderMetadata { + ProviderMetadata::new( + "chatgpt_codex", + "ChatGPT Codex", + "Use your ChatGPT Plus/Pro subscription for GPT-5 Codex models via OAuth", + CHATGPT_CODEX_DEFAULT_MODEL, + CHATGPT_CODEX_KNOWN_MODELS.to_vec(), + CHATGPT_CODEX_DOC_URL, + vec![ConfigKey::new_oauth( + "CHATGPT_CODEX_TOKEN", + true, + true, + None, + )], + ) + } + + fn get_name(&self) -> &str { + &self.name + } + + fn get_model_config(&self) -> ModelConfig { + self.model.clone() + } + + #[tracing::instrument( + skip(self, model_config, system, messages, tools), + fields(model_config, input, output, input_tokens, output_tokens, total_tokens) + )] + async fn complete_with_model( + &self, + _session_id: &str, + model_config: &ModelConfig, + system: &str, + messages: &[Message], + tools: &[Tool], + ) -> Result<(Message, ProviderUsage), ProviderError> { + // ChatGPT Codex API requires streaming - collect the stream into a single response + let mut payload = create_codex_request(model_config, system, messages, tools) + .map_err(|e| ProviderError::ExecutionError(e.to_string()))?; + payload["stream"] = serde_json::Value::Bool(true); + + let response = self + .with_retry(|| async { + let payload_clone = payload.clone(); + self.post_streaming(&payload_clone).await + }) + .await?; + + let stream = response.bytes_stream().map_err(io::Error::other); + let stream_reader = StreamReader::new(stream); + let framed = FramedRead::new(stream_reader, LinesCodec::new()).map_err(anyhow::Error::from); + + let message_stream = responses_api_to_streaming_message(framed); + pin!(message_stream); + + let mut final_message: Option = None; + let mut final_usage: Option = None; + + while let Some(result) = message_stream.next().await { + let (message, usage) = result + .map_err(|e| ProviderError::RequestFailed(format!("Stream decode error: {}", e)))?; + if let Some(msg) = message { + final_message = Some(msg); + } + if let Some(u) = usage { + final_usage = Some(u); + } + } + + let message = final_message.ok_or_else(|| { + ProviderError::ExecutionError("No message received from stream".to_string()) + })?; + let usage = final_usage.unwrap_or_else(|| { + ProviderUsage::new( + model_config.model_name.clone(), + crate::providers::base::Usage::default(), + ) + }); + + Ok((message, usage)) + } + + fn supports_streaming(&self) -> bool { + true + } + + async fn stream( + &self, + _session_id: &str, + system: &str, + messages: &[Message], + tools: &[Tool], + ) -> Result { + let mut payload = create_codex_request(&self.model, system, messages, tools) + .map_err(|e| ProviderError::ExecutionError(e.to_string()))?; + payload["stream"] = serde_json::Value::Bool(true); + + let response = self + .with_retry(|| async { + let payload_clone = payload.clone(); + self.post_streaming(&payload_clone).await + }) + .await?; + + let stream = response.bytes_stream().map_err(io::Error::other); + + Ok(Box::pin(try_stream! { + let stream_reader = StreamReader::new(stream); + let framed = FramedRead::new(stream_reader, LinesCodec::new()).map_err(anyhow::Error::from); + + let message_stream = responses_api_to_streaming_message(framed); + pin!(message_stream); + while let Some(message) = message_stream.next().await { + let (message, usage) = message.map_err(|e| ProviderError::RequestFailed(format!("Stream decode error: {}", e)))?; + yield (message, usage); + } + })) + } + + async fn configure_oauth(&self) -> Result<(), ProviderError> { + self.auth_provider + .get_valid_token() + .await + .map_err(|e| ProviderError::Authentication(format!("OAuth flow failed: {}", e)))?; + Ok(()) + } + + async fn fetch_supported_models( + &self, + _session_id: &str, + ) -> Result>, ProviderError> { + Ok(Some( + CHATGPT_CODEX_KNOWN_MODELS + .iter() + .map(|s| s.to_string()) + .collect(), + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::conversation::message::Message; + use jsonwebtoken::{Algorithm, EncodingKey, Header}; + use rmcp::model::{CallToolRequestParam, CallToolResult, Content, ErrorCode, ErrorData}; + use rmcp::object; + use test_case::test_case; + use wiremock::matchers::{body_string_contains, method, path}; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + fn input_kinds(payload: &Value) -> Vec { + payload["input"] + .as_array() + .map(|items| { + items + .iter() + .map(|item| { + if let Some(role) = item.get("role").and_then(|r| r.as_str()) { + format!("message:{role}") + } else { + item.get("type") + .and_then(|t| t.as_str()) + .unwrap_or("unknown") + .to_string() + } + }) + .collect() + }) + .unwrap_or_default() + } + + #[test_case( + vec![ + Message::user().with_text("user text"), + Message::assistant().with_text("assistant prelude").with_tool_request( + "call-1", + Ok(CallToolRequestParam { + task: None, + name: "tool_name".into(), + arguments: Some(object!({"param": "value"})), + }), + ), + Message::user().with_tool_response( + "call-1", + Ok(CallToolResult::success(vec![Content::text("tool output")])), + ), + Message::assistant().with_text("assistant follow-up"), + ], + vec![ + "message:user".to_string(), + "message:assistant".to_string(), + "function_call".to_string(), + "function_call_output".to_string(), + "message:assistant".to_string(), + ]; + "preserves order when assistant includes text" + )] + #[test_case( + vec![ + Message::user().with_text("user text"), + Message::assistant().with_tool_request( + "call-1", + Ok(CallToolRequestParam { + task: None, + name: "tool_name".into(), + arguments: Some(object!({"param": "value"})), + }), + ), + Message::user().with_tool_response( + "call-1", + Ok(CallToolResult::success(vec![Content::text("tool output")])), + ), + Message::assistant().with_text("assistant follow-up"), + ], + vec![ + "message:user".to_string(), + "function_call".to_string(), + "function_call_output".to_string(), + "message:assistant".to_string(), + ]; + "skips empty assistant message and preserves tool order" + )] + #[test_case( + vec![ + Message::user().with_text("user text"), + Message::assistant().with_tool_request( + "call-1", + Ok(CallToolRequestParam { + task: None, + name: "tool_name".into(), + arguments: Some(object!({"param": "value"})), + }), + ), + Message::user().with_tool_response( + "call-1", + Err(ErrorData::new(ErrorCode::INTERNAL_ERROR, "boom", None)), + ), + ], + vec![ + "message:user".to_string(), + "function_call".to_string(), + "function_call_output".to_string(), + ]; + "includes tool error output" + )] + fn test_codex_input_order(messages: Vec, expected: Vec) { + let items = build_input_items(&messages).unwrap(); + let payload = json!({ "input": items }); + let kinds = input_kinds(&payload); + assert_eq!(kinds, expected); + } + + #[test_case( + JwtClaims { + chatgpt_account_id: Some("account-1".to_string()), + auth_claims: None, + organizations: None, + }, + Some("account-1".to_string()); + "uses top-level account id" + )] + #[test_case( + JwtClaims { + chatgpt_account_id: None, + auth_claims: Some(AuthClaims { + chatgpt_account_id: Some("account-2".to_string()), + }), + organizations: None, + }, + Some("account-2".to_string()); + "uses auth claims account id" + )] + #[test_case( + JwtClaims { + chatgpt_account_id: None, + auth_claims: None, + organizations: Some(vec![OrgInfo { + id: "org-1".to_string(), + }]), + }, + Some("org-1".to_string()); + "falls back to first organization" + )] + fn test_account_id_from_claims(claims: JwtClaims, expected: Option) { + assert_eq!(account_id_from_claims(&claims), expected); + } + + #[tokio::test] + async fn test_exchange_code_for_tokens() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/oauth/token")) + .and(body_string_contains("grant_type=authorization_code")) + .and(body_string_contains("code=code-123")) + .and(body_string_contains( + "redirect_uri=http%3A%2F%2Flocalhost%2Fcallback", + )) + .and(body_string_contains("code_verifier=verifier-123")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "access_token": "access-1", + "refresh_token": "refresh-1", + "id_token": "id-1", + "expires_in": 3600 + }))) + .mount(&server) + .await; + + let pkce = PkceChallenge { + verifier: "verifier-123".to_string(), + challenge: "challenge-123".to_string(), + }; + let tokens = exchange_code_for_tokens_with_issuer( + &server.uri(), + "code-123", + "http://localhost/callback", + &pkce, + ) + .await + .unwrap(); + + assert_eq!(tokens.access_token, "access-1"); + assert_eq!(tokens.refresh_token, "refresh-1"); + assert_eq!(tokens.id_token.as_deref(), Some("id-1")); + assert_eq!(tokens.expires_in, Some(3600)); + } + + #[tokio::test] + async fn test_refresh_access_token() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/oauth/token")) + .and(body_string_contains("grant_type=refresh_token")) + .and(body_string_contains("refresh_token=refresh-123")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "access_token": "access-2", + "refresh_token": "refresh-2", + "id_token": "id-2", + "expires_in": 1800 + }))) + .mount(&server) + .await; + + let tokens = refresh_access_token_with_issuer(&server.uri(), "refresh-123") + .await + .unwrap(); + + assert_eq!(tokens.access_token, "access-2"); + assert_eq!(tokens.refresh_token, "refresh-2"); + assert_eq!(tokens.id_token.as_deref(), Some("id-2")); + assert_eq!(tokens.expires_in, Some(1800)); + } + + #[derive(Serialize)] + struct TestClaims { + exp: usize, + chatgpt_account_id: Option, + } + + #[tokio::test] + async fn test_parse_jwt_claims_verified_with_issuer() { + let server = MockServer::start().await; + let jwks_uri = format!("{}/jwks", server.uri()); + Mock::given(method("GET")) + .and(path("/.well-known/openid-configuration")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "jwks_uri": jwks_uri + }))) + .mount(&server) + .await; + + let secret = "test-secret"; + let key = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(secret); + Mock::given(method("GET")) + .and(path("/jwks")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "keys": [{ + "kty": "oct", + "alg": "HS256", + "kid": "test-kid", + "k": key + }] + }))) + .mount(&server) + .await; + + let mut header = Header::new(Algorithm::HS256); + header.kid = Some("test-kid".to_string()); + + let claims = TestClaims { + exp: (Utc::now() + chrono::Duration::seconds(60)).timestamp() as usize, + chatgpt_account_id: Some("account-1".to_string()), + }; + let token = jsonwebtoken::encode( + &header, + &claims, + &EncodingKey::from_secret(secret.as_bytes()), + ) + .unwrap(); + + let jwks = fetch_jwks_for(&server.uri()).await.unwrap(); + let claims = parse_jwt_claims_with_jwks(&token, &jwks).unwrap(); + + assert_eq!(claims.chatgpt_account_id.as_deref(), Some("account-1")); + } +} diff --git a/crates/goose/src/providers/factory.rs b/crates/goose/src/providers/factory.rs index b051087c120a..286b2ea40edc 100644 --- a/crates/goose/src/providers/factory.rs +++ b/crates/goose/src/providers/factory.rs @@ -5,6 +5,7 @@ use super::{ azure::AzureProvider, base::{Provider, ProviderMetadata}, bedrock::BedrockProvider, + chatgpt_codex::ChatGptCodexProvider, claude_code::ClaudeCodeProvider, codex::CodexProvider, cursor_agent::CursorAgentProvider, @@ -46,6 +47,10 @@ async fn init_registry() -> RwLock { .register::(|m| Box::pin(AnthropicProvider::from_env(m)), true); registry.register::(|m| Box::pin(AzureProvider::from_env(m)), false); registry.register::(|m| Box::pin(BedrockProvider::from_env(m)), false); + registry.register::( + |m| Box::pin(ChatGptCodexProvider::from_env(m)), + true, + ); registry .register::(|m| Box::pin(ClaudeCodeProvider::from_env(m)), true); registry.register::(|m| Box::pin(CodexProvider::from_env(m)), true); diff --git a/crates/goose/src/providers/mod.rs b/crates/goose/src/providers/mod.rs index f1dfeaf9df36..abfc499d657b 100644 --- a/crates/goose/src/providers/mod.rs +++ b/crates/goose/src/providers/mod.rs @@ -6,6 +6,7 @@ pub mod azureauth; pub mod base; pub mod bedrock; pub mod canonical; +pub mod chatgpt_codex; pub mod claude_code; pub mod codex; pub mod cursor_agent; diff --git a/scripts/clippy-baseline.sh b/scripts/clippy-baseline.sh index e0ed230f874c..7d30f3c98ade 100755 --- a/scripts/clippy-baseline.sh +++ b/scripts/clippy-baseline.sh @@ -27,7 +27,9 @@ parse_violation() { case "$violation_parser" in "function_name") jq -r 'select(.message.code.code == "'"$rule_code"'") | - "\(.message.spans[0].file_name)::\(.message.spans[0].text[0].text | split("fn ")[1] | split("(")[0])"' + .message.spans[0] as $span | + ($span.text | map(.text) | map(select(test("\\bfn\\b"))) | first // "") as $line | + if $line == "" then empty else "\($span.file_name)::\($line | capture("fn\\s+(?[a-z_][a-z0-9_]*)") | .name)" end' ;; "type_name") jq -r 'select(.message.code.code == "'"$rule_code"'") | diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 267cdfa8a653..377323813233 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -1214,6 +1214,33 @@ } } }, + "/config/providers/{name}/oauth": { + "post": { + "tags": [ + "super::routes::config_management" + ], + "operationId": "configure_provider_oauth", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Provider name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OAuth configuration completed" + }, + "400": { + "description": "OAuth configuration failed" + } + } + } + }, "/config/read": { "post": { "tags": [ diff --git a/ui/desktop/src/api/index.ts b/ui/desktop/src/api/index.ts index aa0b2b31fe7f..e8ba10190597 100644 --- a/ui/desktop/src/api/index.ts +++ b/ui/desktop/src/api/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { addExtension, agentAddExtension, agentRemoveExtension, backupConfig, callTool, checkProvider, confirmToolAction, createCustomProvider, createRecipe, createSchedule, decodeRecipe, deleteRecipe, deleteSchedule, deleteSession, detectProvider, diagnostics, editMessage, encodeRecipe, exportSession, getCustomProvider, getExtensions, getPricing, getPrompt, getPrompts, getProviderModels, getSession, getSessionExtensions, getSessionInsights, getSlashCommands, getTools, getTunnelStatus, importSession, initConfig, inspectRunningJob, killRunningJob, listApps, listRecipes, listSchedules, listSessions, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, readResource, recipeToYaml, recoverConfig, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, sendTelemetryEvent, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, startOpenrouterSetup, startTetrateSetup, startTunnel, status, stopAgent, stopTunnel, systemInfo, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateSchedule, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; -export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, BackupConfigData, BackupConfigErrors, BackupConfigResponse, BackupConfigResponses, CallToolData, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, ChatRequest, CheckProviderData, CheckProviderRequest, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderRequest, DetectProviderResponse, DetectProviderResponse2, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageRequest, EditMessageResponse, EditMessageResponse2, EditMessageResponses, EditType, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, ErrorResponse, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, FrontendToolRequest, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetPricingData, GetPricingResponse, GetPricingResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, Icon, ImageContent, ImportSessionData, ImportSessionErrors, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponse, InitConfigResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelConfig, ModelInfo, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, PermissionLevel, PricingData, PricingQuery, PricingResponse, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponse, RecoverConfigResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionDisplayInfo, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WindowProps } from './types.gen'; +export { addExtension, agentAddExtension, agentRemoveExtension, backupConfig, callTool, checkProvider, configureProviderOauth, confirmToolAction, createCustomProvider, createRecipe, createSchedule, decodeRecipe, deleteRecipe, deleteSchedule, deleteSession, detectProvider, diagnostics, editMessage, encodeRecipe, exportSession, getCustomProvider, getExtensions, getPricing, getPrompt, getPrompts, getProviderModels, getSession, getSessionExtensions, getSessionInsights, getSlashCommands, getTools, getTunnelStatus, importSession, initConfig, inspectRunningJob, killRunningJob, listApps, listRecipes, listSchedules, listSessions, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, readResource, recipeToYaml, recoverConfig, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, sendTelemetryEvent, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, startOpenrouterSetup, startTetrateSetup, startTunnel, status, stopAgent, stopTunnel, systemInfo, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateSchedule, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; +export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, BackupConfigData, BackupConfigErrors, BackupConfigResponse, BackupConfigResponses, CallToolData, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, ChatRequest, CheckProviderData, CheckProviderRequest, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderRequest, DetectProviderResponse, DetectProviderResponse2, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageRequest, EditMessageResponse, EditMessageResponse2, EditMessageResponses, EditType, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, ErrorResponse, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, FrontendToolRequest, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetPricingData, GetPricingResponse, GetPricingResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, Icon, ImageContent, ImportSessionData, ImportSessionErrors, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponse, InitConfigResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelConfig, ModelInfo, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, PermissionLevel, PricingData, PricingQuery, PricingResponse, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponse, RecoverConfigResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionDisplayInfo, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WindowProps } from './types.gen'; diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 2026f2d8f050..8cdd4bf8939c 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CheckProviderData, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetPricingData, GetPricingResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SystemInfoData, SystemInfoResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; +import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CheckProviderData, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetPricingData, GetPricingResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SystemInfoData, SystemInfoResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -226,6 +226,8 @@ export const providers = (options?: Option export const getProviderModels = (options: Options) => (options.client ?? client).get({ url: '/config/providers/{name}/models', ...options }); +export const configureProviderOauth = (options: Options) => (options.client ?? client).post({ url: '/config/providers/{name}/oauth', ...options }); + export const readConfig = (options: Options) => (options.client ?? client).post({ url: '/config/read', ...options, diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 63a51b16aa8b..9d704768eadf 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -2191,6 +2191,32 @@ export type GetProviderModelsResponses = { export type GetProviderModelsResponse = GetProviderModelsResponses[keyof GetProviderModelsResponses]; +export type ConfigureProviderOauthData = { + body?: never; + path: { + /** + * Provider name + */ + name: string; + }; + query?: never; + url: '/config/providers/{name}/oauth'; +}; + +export type ConfigureProviderOauthErrors = { + /** + * OAuth configuration failed + */ + 400: unknown; +}; + +export type ConfigureProviderOauthResponses = { + /** + * OAuth configuration completed + */ + 200: unknown; +}; + export type ReadConfigData = { body: ConfigKeyQuery; path?: never; diff --git a/ui/desktop/src/components/ApiKeyTester.tsx b/ui/desktop/src/components/ApiKeyTester.tsx index 56ca288c5b59..e26e789ddafa 100644 --- a/ui/desktop/src/components/ApiKeyTester.tsx +++ b/ui/desktop/src/components/ApiKeyTester.tsx @@ -73,20 +73,19 @@ export default function ApiKeyTester({ onSuccess, onStartTesting }: ApiKeyTester -
-
-
- +
+
+ +

Quick Setup with API Key

+ + Auto-detect your provider +
-

- Enter your API key and we'll automatically detect which provider it works with. -

-
(null); + const [chatgptCodexSetupState, setChatgptCodexSetupState] = useState<{ + show: boolean; + title: string; + message: string; + showRetry: boolean; + autoClose?: number; + } | null>(null); + const handleTetrateSetup = async () => { trackOnboardingProviderSelected('tetrate'); try { @@ -97,6 +106,34 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG } }; + const handleChatGptCodexSetup = async () => { + trackOnboardingProviderSelected('chatgpt_codex'); + try { + const result = await startChatGptCodexSetup(); + if (result.success) { + setSwitchModelProvider('chatgpt_codex'); + setShowSwitchModelModal(true); + } else { + trackOnboardingSetupFailed('chatgpt_codex', result.message); + setChatgptCodexSetupState({ + show: true, + title: 'Setup Failed', + message: result.message, + showRetry: true, + }); + } + } catch (error) { + console.error('ChatGPT Codex setup error:', error); + trackOnboardingSetupFailed('chatgpt_codex', 'unexpected_error'); + setChatgptCodexSetupState({ + show: true, + title: 'Setup Error', + message: 'An unexpected error occurred during setup.', + showRetry: true, + }); + } + }; + const handleApiKeySuccess = async (provider: string, _model: string, apiKey: string) => { trackOnboardingProviderSelected('api_key'); const keyName = `${provider.toUpperCase()}_API_KEY`; @@ -163,21 +200,26 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG setShowOllamaSetup(false); }; - const handleRetrySetup = (setupType: 'openrouter' | 'tetrate') => { + const handleRetrySetup = (setupType: 'openrouter' | 'tetrate' | 'chatgpt_codex') => { if (setupType === 'openrouter') { setOpenRouterSetupState(null); handleOpenRouterSetup(); - } else { + } else if (setupType === 'tetrate') { setTetrateSetupState(null); handleTetrateSetup(); + } else { + setChatgptCodexSetupState(null); + handleChatGptCodexSetup(); } }; - const closeSetupModal = (setupType: 'openrouter' | 'tetrate') => { + const closeSetupModal = (setupType: 'openrouter' | 'tetrate' | 'chatgpt_codex') => { if (setupType === 'openrouter') { setOpenRouterSetupState(null); - } else { + } else if (setupType === 'tetrate') { setTetrateSetupState(null); + } else { + setChatgptCodexSetupState(null); } }; @@ -274,9 +316,49 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG }} /> + {/* ChatGPT Subscription Card - Full Width */} +
+
+ + Recommended if you have ChatGPT Plus/Pro + +
+ +
+
+
+ + + ChatGPT Subscription + +
+
+ + + +
+
+

+ Use your ChatGPT Plus/Pro subscription for GPT-5 Codex models. +

+
+
+ {/* Tetrate Card - Full Width */}
- {/* Recommended pill */}
Recommended for new users @@ -415,6 +497,17 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG /> )} + {chatgptCodexSetupState?.show && ( + handleRetrySetup('chatgpt_codex')} + onClose={() => closeSetupModal('chatgpt_codex')} + autoClose={chatgptCodexSetupState.autoClose} + /> + )} + {showSwitchModelModal && ( + + + ); +} diff --git a/ui/desktop/src/components/icons/index.tsx b/ui/desktop/src/components/icons/index.tsx index bbc21ee091d8..c648d2ce6348 100644 --- a/ui/desktop/src/components/icons/index.tsx +++ b/ui/desktop/src/components/icons/index.tsx @@ -2,6 +2,7 @@ import ArrowDown from './ArrowDown'; import ArrowUp from './ArrowUp'; import Attach from './Attach'; import Back from './Back'; +import ChatGPT from './ChatGPT'; import { Bird1 } from './Bird1'; import { Bird2 } from './Bird2'; import { Bird3 } from './Bird3'; @@ -59,6 +60,7 @@ export { ArrowUp, Attach, Back, + ChatGPT, Bird1, Bird2, Bird3, diff --git a/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx b/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx index b1f84e551eb0..c94630811b8f 100644 --- a/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx +++ b/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx @@ -187,7 +187,8 @@ export const SwitchModelModal = ({ // Load providers for manual model selection (async () => { try { - const providersResponse = await getProviders(false); + // Force refresh if initialProvider is set (OAuth flow needs fresh data) + const providersResponse = await getProviders(!!initialProvider); const activeProviders = providersResponse.filter((provider) => provider.is_configured); // Create provider options and add "Use other provider" option setProviderOptions([ @@ -260,7 +261,7 @@ export const SwitchModelModal = ({ setLoadingModels(false); } })(); - }, [getProviders, getProviderModels, usePredefinedModels, read]); + }, [getProviders, getProviderModels, usePredefinedModels, read, initialProvider]); const filteredModelOptions = provider ? modelOptions.filter((group) => group.options[0]?.provider === provider) diff --git a/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx b/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx index a0b76483cad3..358eac25500f 100644 --- a/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx +++ b/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx @@ -16,10 +16,24 @@ import { SecureStorageNotice } from './subcomponents/SecureStorageNotice'; import { providerConfigSubmitHandler } from './subcomponents/handlers/DefaultSubmitHandler'; import { useConfig } from '../../../ConfigContext'; import { useModelAndProvider } from '../../../ModelAndProviderContext'; -import { AlertTriangle } from 'lucide-react'; -import { ProviderDetails, removeCustomProvider } from '../../../../api'; +import { AlertTriangle, LogIn } from 'lucide-react'; +import { ProviderDetails, removeCustomProvider, configureProviderOauth } from '../../../../api'; import { Button } from '../../../../components/ui/button'; +const formatErrorMessage = (error: unknown): string => { + if (error instanceof Error) { + return error.message; + } + if (typeof error === 'string') { + return error; + } + try { + return JSON.stringify(error); + } catch { + return String(error); + } +}; + interface ProviderConfigurationModalProps { provider: ProviderDetails; onClose: () => void; @@ -38,11 +52,15 @@ export default function ProviderConfigurationModal({ const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); const [isActiveProvider, setIsActiveProvider] = useState(false); const [error, setError] = useState(null); + const [isOAuthLoading, setIsOAuthLoading] = useState(false); const requiredParameters = provider.metadata.config_keys.filter( (param) => param.required === true ); + // Check if this provider uses OAuth for configuration + const isOAuthProvider = provider.metadata.config_keys.some((key) => key.oauth_flow); + const isConfigured = provider.is_configured; const headerText = showDeleteConfirmation ? `Delete configuration for ${provider.metadata.display_name}` @@ -52,7 +70,28 @@ export default function ProviderConfigurationModal({ ? isActiveProvider ? `You cannot delete this provider while it's currently in use. Please switch to a different model first.` : 'This will permanently delete the current provider configuration.' - : `Add your API key(s) for this provider to integrate into Goose`; + : isOAuthProvider + ? `Sign in with your ${provider.metadata.display_name} account to use this provider` + : `Add your API key(s) for this provider to integrate into goose`; + + const handleOAuthLogin = async () => { + setIsOAuthLoading(true); + setError(null); + try { + await configureProviderOauth({ + path: { name: provider.name }, + }); + if (onConfigured) { + onConfigured(provider); + } else { + onClose(); + } + } catch (err) { + setError(`OAuth login failed: ${formatErrorMessage(err)}`); + } finally { + setIsOAuthLoading(false); + } +}; const handleSubmitForm = async (e: React.FormEvent) => { e.preventDefault(); @@ -91,7 +130,7 @@ export default function ProviderConfigurationModal({ onClose(); } } catch (error) { - setError(`${error}`); + setError(formatErrorMessage(error)); } }; @@ -179,38 +218,70 @@ export default function ProviderConfigurationModal({ {/* Contains information used to set up each provider */} {/* Only show the form when NOT in delete confirmation mode */} {!showDeleteConfirmation ? ( - <> - {/* Contains information used to set up each provider */} - - - {requiredParameters.length > 0 && - provider.metadata.config_keys && - provider.metadata.config_keys.length > 0 && } - + isOAuthProvider ? ( +
+ +

+ A browser window will open for you to complete the login. +

+
+ ) : ( + <> + {/* Contains information used to set up each provider */} + + + {requiredParameters.length > 0 && + provider.metadata.config_keys && + provider.metadata.config_keys.length > 0 && } + + ) ) : null}
- { - setIsActiveProvider(false); - setShowDeleteConfirmation(false); - }} - canDelete={isConfigured && !isActiveProvider} - providerName={provider.metadata.display_name} - isActiveProvider={isActiveProvider} - /> + {isOAuthProvider && !showDeleteConfirmation ? ( +
+ + {isConfigured && ( + + )} +
+ ) : ( + { + setIsActiveProvider(false); + setShowDeleteConfirmation(false); + }} + canDelete={isConfigured && !isActiveProvider} + providerName={provider.metadata.display_name} + isActiveProvider={isActiveProvider} + /> + )}
diff --git a/ui/desktop/src/utils/analytics.ts b/ui/desktop/src/utils/analytics.ts index b0aa53425ab7..3705f8927884 100644 --- a/ui/desktop/src/utils/analytics.ts +++ b/ui/desktop/src/utils/analytics.ts @@ -69,7 +69,7 @@ export type AnalyticsEvent = | { name: 'onboarding_started'; properties: Record } | { name: 'onboarding_provider_selected'; - properties: { method: 'api_key' | 'openrouter' | 'tetrate' | 'ollama' | 'other' }; + properties: { method: 'api_key' | 'openrouter' | 'tetrate' | 'chatgpt_codex' | 'ollama' | 'other' }; } | { name: 'onboarding_completed'; @@ -78,7 +78,7 @@ export type AnalyticsEvent = | { name: 'onboarding_abandoned'; properties: { step: string; duration_seconds?: number } } | { name: 'onboarding_setup_failed'; - properties: { provider: 'openrouter' | 'tetrate'; error_message?: string }; + properties: { provider: 'openrouter' | 'tetrate' | 'chatgpt_codex'; error_message?: string }; } | { name: 'error_occurred'; @@ -282,7 +282,7 @@ export function trackOnboardingStarted(): void { } export function trackOnboardingProviderSelected( - method: 'api_key' | 'openrouter' | 'tetrate' | 'ollama' | 'other' + method: 'api_key' | 'openrouter' | 'tetrate' | 'chatgpt_codex' | 'ollama' | 'other' ): void { trackEvent({ name: 'onboarding_provider_selected', @@ -315,7 +315,7 @@ export function trackOnboardingAbandoned(step: string): void { } export function trackOnboardingSetupFailed( - provider: 'openrouter' | 'tetrate', + provider: 'openrouter' | 'tetrate' | 'chatgpt_codex', errorMessage?: string ): void { trackEvent({ diff --git a/ui/desktop/src/utils/chatgptCodexSetup.ts b/ui/desktop/src/utils/chatgptCodexSetup.ts new file mode 100644 index 000000000000..2593b0dd9b68 --- /dev/null +++ b/ui/desktop/src/utils/chatgptCodexSetup.ts @@ -0,0 +1,16 @@ +import { configureProviderOauth } from '../api'; + +export async function startChatGptCodexSetup(): Promise<{ success: boolean; message: string }> { + try { + await configureProviderOauth({ + path: { name: 'chatgpt_codex' }, + throwOnError: true, + }); + return { success: true, message: 'ChatGPT Codex setup completed' }; + } catch (e) { + return { + success: false, + message: `Failed to start ChatGPT Codex setup: ${e}`, + }; + } +}