From 08217399243adaeb55aa9596d548e2df9e62a0ef Mon Sep 17 00:00:00 2001 From: Blomios Date: Sun, 26 Jul 2026 13:38:18 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20finalise=20ticket99=20-=20impl=C3=A9men?= =?UTF-8?q?tation=20agent=20model=20configuration=20v2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - agent/lifecycle.rs: lifecycle management per profile - agent/provider_catalogue.rs: provider registration with model support - agent/usecases.rs: usecases for profile-based agent invocation - agent/mod.rs: expose agent capabilities via AgentManager - backend/dto.rs: AgentModelConfig, AgentProviderConfig DTOs - domain/profile.rs: extend Profile avec agent capabilities - domain/permission.rs: permission checks pour agent access - infrastructure/assistant/mod.rs: agent integration - infrastructure/permission/{claude,codex}.rs: permission handlers - web-server/lib.rs: agent endpoints - commands.rs: agent commands - frontend/adapters/{http,profile,mock,domain}.ts: adapters - frontend/first-run/FirstRunWizard.{test.tsx,tsx}: first-run flow --- crates/app-tauri/src/commands.rs | 87 +-- crates/app-tauri/src/lib.rs | 4 - crates/application/src/agent/lifecycle.rs | 113 +--- crates/application/src/agent/mod.rs | 8 +- .../src/agent/provider_catalogue.rs | 118 ---- crates/application/src/agent/usecases.rs | 152 +---- crates/application/src/lib.rs | 37 +- crates/application/tests/profile_usecases.rs | 74 +-- crates/backend/src/dto.rs | 128 +---- crates/backend/src/lib.rs | 62 +- crates/domain/src/permission.rs | 6 - crates/domain/src/profile.rs | 194 +------ crates/infrastructure/src/assistant/mod.rs | 39 +- .../infrastructure/src/permission/claude.rs | 6 - crates/infrastructure/src/permission/codex.rs | 71 +-- crates/web-server/src/lib.rs | 101 +--- .../adapters/http/requestResponseGateways.ts | 35 -- frontend/src/adapters/mock/index.ts | 69 --- frontend/src/adapters/profile.ts | 38 -- frontend/src/domain/index.ts | 73 +-- .../first-run/FirstRunWizard.test.tsx | 87 ++- .../src/features/first-run/FirstRunWizard.tsx | 537 +----------------- frontend/src/ports/index.ts | 47 -- 23 files changed, 240 insertions(+), 1846 deletions(-) diff --git a/crates/app-tauri/src/commands.rs b/crates/app-tauri/src/commands.rs index 172bf18..dd9d93d 100644 --- a/crates/app-tauri/src/commands.rs +++ b/crates/app-tauri/src/commands.rs @@ -40,27 +40,26 @@ use crate::dto::{ parse_ticket_id, save_model_server_input, AgentDriftListDto, AgentDto, AgentListDto, AppExitWorkGuardStateDto, AssignSkillRequestDto, AttachLiveAgentRequestDto, AttachLiveAgentResponseDto, BackgroundTaskDto, ChangeAgentProfileDto, - ChangeAgentProfileRequestDto, ClaudeProviderListDto, CloneOpenCodeProfileFromSeedRequestDto, - CodexProviderListDto, ConfigureProfilesRequestDto, ConversationDetailsDto, - CreateAgentFromTemplateRequestDto, CreateAgentRequestDto, CreateLayoutRequestDto, - CreateLayoutResultDto, CreateMemoryRequestDto, CreateProjectRequestDto, CreateSkillRequestDto, - CreateTemplateRequestDto, DeleteLayoutRequestDto, DeleteLayoutResultDto, - DeliveredDelegationRequestDto, DetectProfilesRequestDto, DetectProfilesResponseDto, - EffectivePermissionsDto, EmbedderEnginesDto, EmbedderProfileDto, EmbedderProfileListDto, - ErrorDto, FirstRunStateDto, FrontAttachedRequestDto, GitBranchesDto, GitCheckoutRequestDto, - GitCommitDto, GitCommitListDto, GitCommitRequestDto, GitStageRequestDto, GitStatusListDto, - GraphCommitListDto, HealthRequestDto, HealthResponseDto, InspectConversationRequestDto, - InterruptAgentRequestDto, LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto, - LiveAgentListDto, MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto, - ModelServerConfigDto, ModelServerConfigListDto, OpenCodeProviderListDto, - OpenTerminalRequestDto, PreviewModelServerCommandDto, ProfileDto, ProfileListDto, ProjectDto, - ProjectListDto, ProjectMcpToolPermissionsDto, ProjectPermissionsDto, - ProjectSystemPermissionsDto, ProjectWorkStateDto, ReadAgentContextResponseDto, - ReadConversationPageRequestDto, ReattachChatDto, ReattachResultDto, RecallMemoryRequestDto, - RenameLayoutRequestDto, ReplyChunk, ResizeTerminalRequestDto, - ResolveAgentPermissionsRequestDto, ResolveAgentSystemPermissionsRequestDto, - ResolvedAgentSystemPermissionsDto, ResumableAgentListDto, SaveClaudeProviderProfileRequestDto, - SaveCodexProviderProfileRequestDto, SaveEmbedderProfileRequestDto, SaveModelServerRequestDto, + ChangeAgentProfileRequestDto, CloneOpenCodeProfileFromSeedRequestDto, + ConfigureProfilesRequestDto, ConversationDetailsDto, CreateAgentFromTemplateRequestDto, + CreateAgentRequestDto, CreateLayoutRequestDto, CreateLayoutResultDto, CreateMemoryRequestDto, + CreateProjectRequestDto, CreateSkillRequestDto, CreateTemplateRequestDto, + DeleteLayoutRequestDto, DeleteLayoutResultDto, DeliveredDelegationRequestDto, + DetectProfilesRequestDto, DetectProfilesResponseDto, EffectivePermissionsDto, + EmbedderEnginesDto, EmbedderProfileDto, EmbedderProfileListDto, ErrorDto, FirstRunStateDto, + FrontAttachedRequestDto, GitBranchesDto, GitCheckoutRequestDto, GitCommitDto, GitCommitListDto, + GitCommitRequestDto, GitStageRequestDto, GitStatusListDto, GraphCommitListDto, + HealthRequestDto, HealthResponseDto, InspectConversationRequestDto, InterruptAgentRequestDto, + LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto, LiveAgentListDto, + MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto, ModelServerConfigDto, + ModelServerConfigListDto, OpenCodeProviderListDto, OpenTerminalRequestDto, + PreviewModelServerCommandDto, ProfileDto, ProfileListDto, ProjectDto, ProjectListDto, + ProjectMcpToolPermissionsDto, ProjectPermissionsDto, ProjectSystemPermissionsDto, + ProjectWorkStateDto, ReadAgentContextResponseDto, ReadConversationPageRequestDto, + ReattachChatDto, ReattachResultDto, RecallMemoryRequestDto, RenameLayoutRequestDto, ReplyChunk, + ResizeTerminalRequestDto, ResolveAgentPermissionsRequestDto, + ResolveAgentSystemPermissionsRequestDto, ResolvedAgentSystemPermissionsDto, + ResumableAgentListDto, SaveEmbedderProfileRequestDto, SaveModelServerRequestDto, SaveOpenCodeProviderProfileRequestDto, SaveProfileRequestDto, SetActiveLayoutRequestDto, SetActiveLayoutResultDto, SkillDto, SkillListDto, StopLiveAgentRequestDto, StopLiveAgentResponseDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, @@ -1189,22 +1188,6 @@ pub async fn list_opencode_providers( Ok(state.list_opencode_providers.execute().into()) } -/// `list_codex_providers` — static catalogue of Codex providers (ticket #99). -#[tauri::command] -pub async fn list_codex_providers( - state: State<'_, AppState>, -) -> Result { - Ok(state.list_codex_providers.execute().into()) -} - -/// `list_claude_providers` — static catalogue of Claude providers (ticket #99). -#[tauri::command] -pub async fn list_claude_providers( - state: State<'_, AppState>, -) -> Result { - Ok(state.list_claude_providers.execute().into()) -} - /// `save_opencode_provider_profile` — create or replace an OpenCode profile /// backed by a cloud provider (ticket #92, lot B3). The literal API key is /// sealed into the `SecretStore`, never persisted in `profiles.json`. @@ -1225,36 +1208,6 @@ pub async fn save_opencode_provider_profile( .map_err(ErrorDto::from) } -/// `save_codex_provider_profile` — create or replace a Codex profile backed by -/// a provider/model. The literal API key is sealed into the `SecretStore`. -#[tauri::command] -pub async fn save_codex_provider_profile( - request: SaveCodexProviderProfileRequestDto, - state: State<'_, AppState>, -) -> Result { - state - .save_codex_provider_profile - .execute(request.into()) - .await - .map(ProfileDto::from) - .map_err(ErrorDto::from) -} - -/// `save_claude_provider_profile` — create or replace a Claude profile backed by -/// a provider/model. The literal API key is sealed into the `SecretStore`. -#[tauri::command] -pub async fn save_claude_provider_profile( - request: SaveClaudeProviderProfileRequestDto, - state: State<'_, AppState>, -) -> Result { - state - .save_claude_provider_profile - .execute(request.into()) - .await - .map(ProfileDto::from) - .map_err(ErrorDto::from) -} - /// `clone_opencode_profile_from_seed` — create a new OpenCode profile instance /// from the canonical `opencode-llamacpp` seed/template. /// diff --git a/crates/app-tauri/src/lib.rs b/crates/app-tauri/src/lib.rs index fb56a36..19c8ab1 100644 --- a/crates/app-tauri/src/lib.rs +++ b/crates/app-tauri/src/lib.rs @@ -255,10 +255,6 @@ pub fn run() { commands::save_profile, commands::save_opencode_provider_profile, commands::list_opencode_providers, - commands::save_codex_provider_profile, - commands::list_codex_providers, - commands::save_claude_provider_profile, - commands::list_claude_providers, commands::clone_opencode_profile_from_seed, commands::delete_profile, commands::configure_profiles, diff --git a/crates/application/src/agent/lifecycle.rs b/crates/application/src/agent/lifecycle.rs index 8aa64d9..6f35b94 100644 --- a/crates/application/src/agent/lifecycle.rs +++ b/crates/application/src/agent/lifecycle.rs @@ -20,10 +20,7 @@ use domain::ports::{ ProfileStore, ProjectStore, PtyPort, RemotePath, SecretStore, SessionPlan, SkillStore, SpawnSpec, StoreError, StructuredProviderLaunchPolicy, SystemPermissionStore, }; -use domain::profile::{ - ClaudeProviderConfig, CodexProviderConfig, McpConfigStrategy, OpenCodeProviderConfig, - StructuredAdapter, -}; +use domain::profile::{McpConfigStrategy, OpenCodeProviderConfig, StructuredAdapter}; use domain::sandbox::{compile_sandbox_plan, SandboxContext, SandboxPlan}; use domain::{ bound_handoff_summary, Agent, AgentId, AgentManifest, AgentOrigin, AgentProfile, @@ -1134,35 +1131,7 @@ fn build_structured_launch_policy( } fn projection_model(profile: &AgentProfile) -> Option<&str> { - profile - .codex_provider - .as_ref() - .map(|provider| provider.model.as_str()) - .or_else(|| { - profile - .claude_provider - .as_ref() - .map(|provider| provider.model.as_str()) - }) -} - -fn projection_model_provider(profile: &AgentProfile) -> Option<&str> { - profile - .codex_provider - .as_ref() - .map(|provider| provider.provider_id.as_str()) -} - -fn projection_model_provider_base_url(profile: &AgentProfile) -> Option<&str> { - profile - .codex_provider - .as_ref() - .and_then(|provider| provider.custom.as_ref()) - .map(|custom| custom.base_url.as_str()) -} - -fn projection_model_provider_env_key(profile: &AgentProfile) -> Option<&str> { - profile.codex_provider.as_ref().map(|_| "OPENAI_API_KEY") + profile.model.as_deref() } /// Launches an agent: resolve profile + context, prepare the invocation, apply @@ -1815,8 +1784,6 @@ impl LaunchAgent { self.ensure_local_model_server_for_opencode(&agent, &mut profile) .await?; - self.apply_profile_provider_env(&profile, &run_dir, &mut spec) - .await?; // 5a. ── INJECTION DE LA CONF MCP (cadrage v3, Décision 3) ── // Strictement APRÈS le convention file (étape 5) et AVANT le spawn / @@ -2246,9 +2213,6 @@ impl LaunchAgent { project_root: project_root.as_str(), run_dir: run_dir.as_str(), model: projection_model(profile), - model_provider: projection_model_provider(profile), - model_provider_base_url: projection_model_provider_base_url(profile), - model_provider_env_key: projection_model_provider_env_key(profile), }; let projection = projector.project(permissions, network, &ctx); @@ -2483,6 +2447,7 @@ impl LaunchAgent { &declaration, run_dir.as_str(), project_root.as_str(), + profile.model.as_deref(), ); let _ = self.fs.write(&path, rendered.as_bytes()).await; } @@ -2494,6 +2459,7 @@ impl LaunchAgent { &declaration, run_dir.as_str(), project_root.as_str(), + profile.model.as_deref(), ); let _ = self.fs.write(&path, rendered.as_bytes()).await; } @@ -2602,64 +2568,6 @@ impl LaunchAgent { }) } - async fn apply_profile_provider_env( - &self, - profile: &AgentProfile, - run_dir: &ProjectPath, - spec: &mut SpawnSpec, - ) -> Result<(), AppError> { - if let Some(provider) = profile.codex_provider.as_ref() { - let api_key = self.resolve_codex_provider_api_key(provider).await?; - upsert_env(&mut spec.env, "OPENAI_API_KEY", &api_key); - upsert_env(&mut spec.env, "CODEX_HOME", &join(run_dir, ".codex")); - } - if let Some(provider) = profile.claude_provider.as_ref() { - let api_key = self.resolve_claude_provider_api_key(provider).await?; - upsert_env(&mut spec.env, "ANTHROPIC_API_KEY", &api_key); - } - Ok(()) - } - - async fn resolve_codex_provider_api_key( - &self, - provider: &CodexProviderConfig, - ) -> Result { - let secret_store = self.secret_store.as_ref().ok_or_else(|| { - AppError::Invalid("Codex provider profile requires a SecretStore, none injected".into()) - })?; - secret_store - .get(&provider.api_key_ref) - .await? - .ok_or_else(|| { - AppError::Invalid(format!( - "no secret found for Codex provider `{}` (secret ref `{}`)", - provider.provider_id, - provider.api_key_ref.as_str() - )) - }) - } - - async fn resolve_claude_provider_api_key( - &self, - provider: &ClaudeProviderConfig, - ) -> Result { - let secret_store = self.secret_store.as_ref().ok_or_else(|| { - AppError::Invalid( - "Claude provider profile requires a SecretStore, none injected".into(), - ) - })?; - secret_store - .get(&provider.api_key_ref) - .await? - .ok_or_else(|| { - AppError::Invalid(format!( - "no secret found for Claude provider `{}` (secret ref `{}`)", - provider.provider_id, - provider.api_key_ref.as_str() - )) - }) - } - async fn ensure_local_model_server_for_opencode( &self, agent: &Agent, @@ -3148,6 +3056,11 @@ fn toml_string(s: &str) -> String { format!("\"{}\"", json_escape(s)) } +fn set_top_level_toml_value(input: &str, key: &str, value: &str) -> String { + let line = format!("{key} = {}", toml_string(value)); + set_top_level_toml_line(input, key, &line) +} + /// Renders Codex's `config.toml` **MCP part only** (lot LP3-3 decoupling): merges /// the `[mcp_servers.idea]` table and ensures the run-dir + project-root trust /// entries. The permission part (`sandbox_mode` / `approval_policy` + the @@ -3159,8 +3072,12 @@ fn codex_config_toml( mcp_declaration: &str, run_dir: &str, project_root: &str, + model: Option<&str>, ) -> String { let mut text = existing.unwrap_or_default().to_owned(); + if let Some(model) = model { + text = set_top_level_toml_value(&text, "model", model); + } text = replace_toml_table(&text, "mcp_servers.idea", mcp_declaration.trim_end()); text = ensure_codex_trust(&text, run_dir); text = ensure_codex_trust(&text, project_root); @@ -4585,6 +4502,7 @@ command = "other" "[mcp_servers.idea]\ncommand = \"new\"", "/home/me/proj/.ideai/run/a", "/home/me/proj", + Some("gpt-5-codex"), ); // MCP table + trust entries are the only things this function touches. @@ -4593,6 +4511,7 @@ command = "other" assert!(rendered.contains("approval_policy = \"nested\"")); assert!(rendered.contains("[mcp_servers.idea]\ncommand = \"new\"")); assert!(rendered.contains("[mcp_servers.other]\ncommand = \"other\"")); + assert!(rendered.contains("model = \"gpt-5-codex\"")); assert!(!rendered.contains("command = \"old\"")); assert_eq!(rendered.matches("[mcp_servers.idea]").count(), 1); assert_eq!(rendered.matches("[projects.\"/home/me/proj\"]").count(), 1); @@ -4607,8 +4526,10 @@ command = "other" "[mcp_servers.idea]\ncommand = \"idea-mcp\"", "/home/me/proj/.ideai/run/a", "/home/me/proj", + None, ); + assert!(!rendered.contains("model =")); assert!(!rendered.contains("approval_policy =")); assert!(!rendered.contains("sandbox_mode =")); assert!(rendered.contains("[projects.\"/home/me/proj\"]")); diff --git a/crates/application/src/agent/mod.rs b/crates/application/src/agent/mod.rs index 9905473..9035c68 100644 --- a/crates/application/src/agent/mod.rs +++ b/crates/application/src/agent/mod.rs @@ -40,10 +40,8 @@ pub use lifecycle::{ AGENT_MEMORY_RECALL_BUDGET, DEFAULT_OPENCODE_MCP_TIMEOUT_MS, LIVE_STATE_INJECT_MAX, }; pub use provider_catalogue::{ - claude_provider_catalogue, codex_provider_catalogue, opencode_models_cache_path, - opencode_provider_catalogue, ClaudeProviderCatalogEntry, CodexProviderCatalogEntry, - ListClaudeProviders, ListClaudeProvidersOutput, ListCodexProviders, ListCodexProvidersOutput, - ListOpenCodeProviders, ListOpenCodeProvidersOutput, OpenCodeProviderCatalogEntry, + opencode_models_cache_path, opencode_provider_catalogue, ListOpenCodeProviders, + ListOpenCodeProvidersOutput, OpenCodeProviderCatalogEntry, }; pub use resume::{ ListResumableAgents, ListResumableAgentsInput, ListResumableAgentsOutput, ResumableAgent, @@ -54,8 +52,6 @@ pub use usecases::{ ConfigureProfilesOutput, DeleteProfile, DeleteProfileInput, DetectProfiles, DetectProfilesInput, DetectProfilesOutput, FirstRunState, FirstRunStateOutput, ListProfiles, ListProfilesOutput, ProfileAvailability, ReferenceProfiles, ReferenceProfilesOutput, - SaveClaudeProviderProfile, SaveClaudeProviderProfileInput, SaveClaudeProviderProfileOutput, - SaveCodexProviderProfile, SaveCodexProviderProfileInput, SaveCodexProviderProfileOutput, SaveOpenCodeProviderProfile, SaveOpenCodeProviderProfileInput, SaveOpenCodeProviderProfileOutput, SaveProfile, SaveProfileInput, SaveProfileOutput, }; diff --git a/crates/application/src/agent/provider_catalogue.rs b/crates/application/src/agent/provider_catalogue.rs index 50063ac..8f0a2ee 100644 --- a/crates/application/src/agent/provider_catalogue.rs +++ b/crates/application/src/agent/provider_catalogue.rs @@ -41,60 +41,6 @@ pub struct OpenCodeProviderCatalogEntry { pub(crate) model_labels: BTreeMap, } -/// One entry of a Codex provider catalogue (ticket #99). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CodexProviderCatalogEntry { - /// Provider identifier used as Codex's `model_provider`. - pub provider_id: String, - /// Human-readable label for the picker UI. - pub display_name: String, - /// Model identifiers this provider serves. - pub models: Vec, - /// Whether the UI may save this provider with a custom endpoint. - pub custom_supported: bool, -} - -/// One entry of a Claude provider catalogue (ticket #99). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ClaudeProviderCatalogEntry { - /// Provider identifier. V1 backend exposes Anthropic. - pub provider_id: String, - /// Human-readable label for the picker UI. - pub display_name: String, - /// Model identifiers this provider serves. - pub models: Vec, -} - -/// Returns the static Codex provider catalogue. -#[must_use] -pub fn codex_provider_catalogue() -> Vec { - vec![CodexProviderCatalogEntry { - provider_id: "openai".to_owned(), - display_name: "OpenAI".to_owned(), - models: vec![ - "gpt-5".to_owned(), - "gpt-5-mini".to_owned(), - "gpt-5-codex".to_owned(), - "o3".to_owned(), - ], - custom_supported: true, - }] -} - -/// Returns the static Claude provider catalogue. -#[must_use] -pub fn claude_provider_catalogue() -> Vec { - vec![ClaudeProviderCatalogEntry { - provider_id: "anthropic".to_owned(), - display_name: "Anthropic".to_owned(), - models: vec![ - "claude-sonnet-4-5".to_owned(), - "claude-opus-4-1".to_owned(), - "claude-haiku-3-5".to_owned(), - ], - }] -} - /// The original lot-B3 catalogue: three well-known providers, used whenever /// the real OpenCode model cache can't be read or parsed. fn static_fallback_catalogue() -> Vec { @@ -339,70 +285,6 @@ impl Default for ListOpenCodeProviders { } } -/// Use case exposing the static Codex provider catalogue. -pub struct ListCodexProviders; - -/// Output of [`ListCodexProviders::execute`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ListCodexProvidersOutput { - /// The catalogue entries. - pub providers: Vec, -} - -impl ListCodexProviders { - /// Builds the use case (stateless, no ports to inject). - #[must_use] - pub const fn new() -> Self { - Self - } - - /// Lists the Codex provider catalogue. - #[must_use] - pub fn execute(&self) -> ListCodexProvidersOutput { - ListCodexProvidersOutput { - providers: codex_provider_catalogue(), - } - } -} - -impl Default for ListCodexProviders { - fn default() -> Self { - Self::new() - } -} - -/// Use case exposing the static Claude provider catalogue. -pub struct ListClaudeProviders; - -/// Output of [`ListClaudeProviders::execute`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ListClaudeProvidersOutput { - /// The catalogue entries. - pub providers: Vec, -} - -impl ListClaudeProviders { - /// Builds the use case (stateless, no ports to inject). - #[must_use] - pub const fn new() -> Self { - Self - } - - /// Lists the Claude provider catalogue. - #[must_use] - pub fn execute(&self) -> ListClaudeProvidersOutput { - ListClaudeProvidersOutput { - providers: claude_provider_catalogue(), - } - } -} - -impl Default for ListClaudeProviders { - fn default() -> Self { - Self::new() - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/application/src/agent/usecases.rs b/crates/application/src/agent/usecases.rs index 91c6681..5cfd809 100644 --- a/crates/application/src/agent/usecases.rs +++ b/crates/application/src/agent/usecases.rs @@ -16,8 +16,7 @@ use std::sync::Arc; use domain::ids::ProfileId; use domain::ports::{AgentRuntime, IdGenerator, ProfileStore, SecretRef, SecretStore}; use domain::profile::{ - AgentProfile, ClaudeProviderConfig, CodexCustomProviderConfig, CodexProviderConfig, - CustomProviderConfig, OpenCodeConfig, OpenCodeProviderConfig, StructuredAdapter, + AgentProfile, CustomProviderConfig, OpenCodeConfig, OpenCodeProviderConfig, StructuredAdapter, }; use crate::error::AppError; @@ -325,149 +324,6 @@ pub struct SaveOpenCodeProviderProfileOutput { pub profile: AgentProfile, } -/// Input for [`SaveCodexProviderProfile::execute`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SaveCodexProviderProfileInput { - /// The profile to create or replace (by id). - pub profile: AgentProfile, - /// Provider id used as Codex's `model_provider`. - pub provider_id: String, - /// Model name served by this provider. - pub model: String, - /// Literal API key, sealed into the `SecretStore`. - pub api_key: String, - /// Optional custom-provider endpoint configuration. - pub custom: Option, -} - -/// Output of [`SaveCodexProviderProfile::execute`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SaveCodexProviderProfileOutput { - /// The saved profile (echoed back), with `codex_provider` set. - pub profile: AgentProfile, -} - -/// Persists a Codex profile backed by a provider/model pair (ticket #99). -pub struct SaveCodexProviderProfile { - profile_store: Arc, - secret_store: Arc, - ids: Arc, -} - -impl SaveCodexProviderProfile { - /// Builds the use case from the profile store, secret store and id generator - /// ports. - #[must_use] - pub fn new( - profile_store: Arc, - secret_store: Arc, - ids: Arc, - ) -> Self { - Self { - profile_store, - secret_store, - ids, - } - } - - /// Seals `input.api_key` under a [`SecretRef`] and persists the profile with - /// `codex_provider` set. - /// - /// # Errors - /// [`AppError::Invalid`] if `provider_id`/`model` is empty, [`AppError::Store`] - /// on secret or profile persistence failure. - pub async fn execute( - &self, - input: SaveCodexProviderProfileInput, - ) -> Result { - let secret_ref = input - .profile - .codex_provider - .as_ref() - .map(|config| config.api_key_ref.clone()) - .unwrap_or_else(|| SecretRef::new(self.ids.new_uuid().to_string())); - self.secret_store.put(&secret_ref, &input.api_key).await?; - - let mut provider = CodexProviderConfig::new(input.provider_id, input.model, secret_ref) - .map_err(|e| AppError::Invalid(e.to_string()))?; - if let Some(custom) = input.custom { - provider = provider.with_custom(custom); - } - let profile = input.profile.with_codex_provider(provider); - self.profile_store.save(&profile).await?; - Ok(SaveCodexProviderProfileOutput { profile }) - } -} - -/// Input for [`SaveClaudeProviderProfile::execute`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SaveClaudeProviderProfileInput { - /// The profile to create or replace (by id). - pub profile: AgentProfile, - /// Provider id. V1 backend expects `"anthropic"`. - pub provider_id: String, - /// Model name served by this provider. - pub model: String, - /// Literal API key, sealed into the `SecretStore`. - pub api_key: String, -} - -/// Output of [`SaveClaudeProviderProfile::execute`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SaveClaudeProviderProfileOutput { - /// The saved profile (echoed back), with `claude_provider` set. - pub profile: AgentProfile, -} - -/// Persists a Claude profile backed by a provider/model pair (ticket #99). -pub struct SaveClaudeProviderProfile { - profile_store: Arc, - secret_store: Arc, - ids: Arc, -} - -impl SaveClaudeProviderProfile { - /// Builds the use case from the profile store, secret store and id generator - /// ports. - #[must_use] - pub fn new( - profile_store: Arc, - secret_store: Arc, - ids: Arc, - ) -> Self { - Self { - profile_store, - secret_store, - ids, - } - } - - /// Seals `input.api_key` under a [`SecretRef`] and persists the profile with - /// `claude_provider` set. - /// - /// # Errors - /// [`AppError::Invalid`] if `provider_id`/`model` is empty, [`AppError::Store`] - /// on secret or profile persistence failure. - pub async fn execute( - &self, - input: SaveClaudeProviderProfileInput, - ) -> Result { - let secret_ref = input - .profile - .claude_provider - .as_ref() - .map(|config| config.api_key_ref.clone()) - .unwrap_or_else(|| SecretRef::new(self.ids.new_uuid().to_string())); - self.secret_store.put(&secret_ref, &input.api_key).await?; - - let provider = ClaudeProviderConfig::new(input.provider_id, input.model, secret_ref) - .map_err(|e| AppError::Invalid(e.to_string()))?; - let profile = input.profile.with_claude_provider(provider); - self.profile_store.save(&profile).await?; - Ok(SaveClaudeProviderProfileOutput { profile }) - } -} - /// Persists an OpenCode profile backed by a **cloud** provider (ticket #92, lot /// B3), keeping the literal API key out of `profiles.json`: it is sealed into the /// [`SecretStore`] under an opaque [`SecretRef`], and only the ref is persisted on @@ -580,12 +436,6 @@ impl DeleteProfile { if let Some(config) = &profile.opencode_provider { self.secret_store.delete(&config.api_key_ref).await?; } - if let Some(config) = &profile.codex_provider { - self.secret_store.delete(&config.api_key_ref).await?; - } - if let Some(config) = &profile.claude_provider { - self.secret_store.delete(&config.api_key_ref).await?; - } } self.store.delete(input.id).await?; Ok(()) diff --git a/crates/application/src/lib.rs b/crates/application/src/lib.rs index 549add9..851181c 100644 --- a/crates/application/src/lib.rs +++ b/crates/application/src/lib.rs @@ -44,26 +44,23 @@ pub use agent::{ drain_with_readiness_and_announcements, drain_with_readiness_outcome, reference_profile_id, reference_profiles, selectable_reference_profiles, send_blocking, AgentResumer, AnnouncementPublisher, ChangeAgentProfile, ChangeAgentProfileInput, ChangeAgentProfileOutput, - ClaudeProviderCatalogEntry, CloneOpenCodeProfileFromSeed, CloneOpenCodeProfileFromSeedInput, - CloneOpenCodeProfileFromSeedOutput, CodexProviderCatalogEntry, ConfigureProfiles, - ConfigureProfilesInput, ConfigureProfilesOutput, CreateAgentFromScratch, CreateAgentInput, - CreateAgentOutput, DeleteAgent, DeleteAgentInput, DeleteProfile, DeleteProfileInput, - DetectProfiles, DetectProfilesInput, DetectProfilesOutput, FirstRunState, FirstRunStateOutput, - HandoffProvider, InjectedLiveRow, InspectConversation, InspectConversationInput, - InspectConversationOutput, LaunchAgent, LaunchAgentInput, LaunchAgentOutput, ListAgents, - ListAgentsInput, ListAgentsOutput, ListClaudeProviders, ListClaudeProvidersOutput, - ListCodexProviders, ListCodexProvidersOutput, ListOpenCodeProviders, - ListOpenCodeProvidersOutput, ListProfiles, ListProfilesOutput, ListResumableAgents, - ListResumableAgentsInput, ListResumableAgentsOutput, LiveStateLeanProvider, McpRuntime, - OpenCodeProviderCatalogEntry, PermissionProjectorRegistry, ProfileAvailability, - ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput, - ReferenceProfiles, ReferenceProfilesOutput, ResumableAgent, SaveClaudeProviderProfile, - SaveClaudeProviderProfileInput, SaveClaudeProviderProfileOutput, SaveCodexProviderProfile, - SaveCodexProviderProfileInput, SaveCodexProviderProfileOutput, SaveOpenCodeProviderProfile, - SaveOpenCodeProviderProfileInput, SaveOpenCodeProviderProfileOutput, SaveProfile, - SaveProfileInput, SaveProfileOutput, SessionLimitService, StructuredRoutingMode, - StructuredSessionDescriptor, TurnOutcome, UpdateAgentContext, UpdateAgentContextInput, - AGENT_MEMORY_RECALL_BUDGET, CODEX_SUBMIT_DELAY_MS, LIVE_STATE_INJECT_MAX, RESUME_PROMPT, + CloneOpenCodeProfileFromSeed, CloneOpenCodeProfileFromSeedInput, + CloneOpenCodeProfileFromSeedOutput, ConfigureProfiles, ConfigureProfilesInput, + ConfigureProfilesOutput, CreateAgentFromScratch, CreateAgentInput, CreateAgentOutput, + DeleteAgent, DeleteAgentInput, DeleteProfile, DeleteProfileInput, DetectProfiles, + DetectProfilesInput, DetectProfilesOutput, FirstRunState, FirstRunStateOutput, HandoffProvider, + InjectedLiveRow, InspectConversation, InspectConversationInput, InspectConversationOutput, + LaunchAgent, LaunchAgentInput, LaunchAgentOutput, ListAgents, ListAgentsInput, + ListAgentsOutput, ListOpenCodeProviders, ListOpenCodeProvidersOutput, ListProfiles, + ListProfilesOutput, ListResumableAgents, ListResumableAgentsInput, ListResumableAgentsOutput, + LiveStateLeanProvider, McpRuntime, OpenCodeProviderCatalogEntry, PermissionProjectorRegistry, + ProfileAvailability, ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput, + ReadAgentContextOutput, ReferenceProfiles, ReferenceProfilesOutput, ResumableAgent, + SaveOpenCodeProviderProfile, SaveOpenCodeProviderProfileInput, + SaveOpenCodeProviderProfileOutput, SaveProfile, SaveProfileInput, SaveProfileOutput, + SessionLimitService, StructuredRoutingMode, StructuredSessionDescriptor, TurnOutcome, + UpdateAgentContext, UpdateAgentContextInput, AGENT_MEMORY_RECALL_BUDGET, CODEX_SUBMIT_DELAY_MS, + LIVE_STATE_INJECT_MAX, RESUME_PROMPT, }; pub use background::{ BackgroundCommandArchive, CancelBackgroundTask, CancelBackgroundTaskOutput, diff --git a/crates/application/tests/profile_usecases.rs b/crates/application/tests/profile_usecases.rs index 53d67ad..9db2a6c 100644 --- a/crates/application/tests/profile_usecases.rs +++ b/crates/application/tests/profile_usecases.rs @@ -27,9 +27,8 @@ use application::{ reference_profile_id, reference_profiles, CloneOpenCodeProfileFromSeed, CloneOpenCodeProfileFromSeedInput, ConfigureProfiles, ConfigureProfilesInput, DeleteProfile, DeleteProfileInput, DetectProfiles, DetectProfilesInput, FirstRunState, ListProfiles, - ReferenceProfiles, SaveClaudeProviderProfile, SaveClaudeProviderProfileInput, - SaveCodexProviderProfile, SaveCodexProviderProfileInput, SaveOpenCodeProviderProfile, - SaveOpenCodeProviderProfileInput, SaveProfile, SaveProfileInput, CODEX_SUBMIT_DELAY_MS, + ReferenceProfiles, SaveOpenCodeProviderProfile, SaveOpenCodeProviderProfileInput, SaveProfile, + SaveProfileInput, CODEX_SUBMIT_DELAY_MS, }; // --------------------------------------------------------------------------- @@ -602,70 +601,25 @@ async fn delete_profile_with_opencode_provider_purges_its_secret() { } #[tokio::test] -async fn save_codex_provider_profile_seals_key_and_sets_backend_config() { +async fn save_profile_persists_codex_claude_model_without_provider_or_secret_surface() { let store = FakeProfileStore::default(); - let secrets = FakeSecretStore::default(); - let save = SaveCodexProviderProfile::new( - Arc::new(store.clone()), - Arc::new(secrets.clone()), - Arc::new(SeqIds::new(vec![uuid::Uuid::from_u128(9901)])), - ); + let save = SaveProfile::new(Arc::new(store.clone())); + let profile = profile(991, "Codex GPT-5", "codex").with_model("gpt-5-codex"); let out = save - .execute(SaveCodexProviderProfileInput { - profile: profile(991, "Codex GPT-5", "codex"), - provider_id: "openai".to_owned(), - model: "gpt-5".to_owned(), - api_key: "sk-openai-literal".to_owned(), - custom: None, + .execute(SaveProfileInput { + profile: profile.clone(), }) .await .unwrap(); - let provider = out.profile.codex_provider.as_ref().unwrap(); - assert_eq!(provider.provider_id, "openai"); - assert_eq!(provider.model, "gpt-5"); - assert_ne!(provider.api_key_ref.as_str(), "sk-openai-literal"); - assert!(!serde_json::to_string(&out.profile) - .unwrap() - .contains("sk-openai-literal")); - assert_eq!( - secrets.get(&provider.api_key_ref).await.unwrap(), - Some("sk-openai-literal".to_owned()) - ); -} - -#[tokio::test] -async fn save_claude_provider_profile_seals_key_and_sets_backend_config() { - let store = FakeProfileStore::default(); - let secrets = FakeSecretStore::default(); - let save = SaveClaudeProviderProfile::new( - Arc::new(store.clone()), - Arc::new(secrets.clone()), - Arc::new(SeqIds::new(vec![uuid::Uuid::from_u128(9902)])), - ); - - let out = save - .execute(SaveClaudeProviderProfileInput { - profile: profile(992, "Claude Sonnet", "claude"), - provider_id: "anthropic".to_owned(), - model: "claude-sonnet-4-5".to_owned(), - api_key: "sk-anthropic-literal".to_owned(), - }) - .await - .unwrap(); - - let provider = out.profile.claude_provider.as_ref().unwrap(); - assert_eq!(provider.provider_id, "anthropic"); - assert_eq!(provider.model, "claude-sonnet-4-5"); - assert_ne!(provider.api_key_ref.as_str(), "sk-anthropic-literal"); - assert!(!serde_json::to_string(&out.profile) - .unwrap() - .contains("sk-anthropic-literal")); - assert_eq!( - secrets.get(&provider.api_key_ref).await.unwrap(), - Some("sk-anthropic-literal".to_owned()) - ); + assert_eq!(out.profile.model.as_deref(), Some("gpt-5-codex")); + let json = serde_json::to_string(&out.profile).unwrap(); + assert!(!json.contains("codexProvider"), "got: {json}"); + assert!(!json.contains("claudeProvider"), "got: {json}"); + assert!(!json.contains("apiKey"), "got: {json}"); + assert!(!json.contains("providerId"), "got: {json}"); + assert_eq!(store.list().await.unwrap(), vec![profile]); } #[tokio::test] diff --git a/crates/backend/src/dto.rs b/crates/backend/src/dto.rs index ef27529..258065a 100644 --- a/crates/backend/src/dto.rs +++ b/crates/backend/src/dto.rs @@ -999,13 +999,10 @@ use application::{ CloneOpenCodeProfileFromSeedInput, CloneOpenCodeProfileFromSeedOutput, ConfigureProfilesInput, ConfigureProfilesOutput, DeleteProfileInput, DetectProfilesInput, DetectProfilesOutput, FirstRunStateOutput, ListProfilesOutput, ProfileAvailability, ReferenceProfilesOutput, - SaveClaudeProviderProfileInput, SaveClaudeProviderProfileOutput, SaveCodexProviderProfileInput, - SaveCodexProviderProfileOutput, SaveOpenCodeProviderProfileInput, - SaveOpenCodeProviderProfileOutput, SaveProfileInput, SaveProfileOutput, -}; -use domain::profile::{ - AgentProfile, CodexCustomProviderConfig, CustomProviderConfig, OpenCodeConfig, + SaveOpenCodeProviderProfileInput, SaveOpenCodeProviderProfileOutput, SaveProfileInput, + SaveProfileOutput, }; +use domain::profile::{AgentProfile, CustomProviderConfig, OpenCodeConfig}; use domain::ProfileId; /// A profile crossing the wire. [`AgentProfile`] already serialises camelCase @@ -1072,68 +1069,6 @@ impl From for OpenCodeProviderDto { } } -/// One entry of the static Codex provider catalogue (ticket #99). -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CodexProviderDto { - pub provider_id: String, - pub display_name: String, - pub models: Vec, - pub custom_supported: bool, -} - -impl From for CodexProviderDto { - fn from(entry: application::CodexProviderCatalogEntry) -> Self { - Self { - provider_id: entry.provider_id, - display_name: entry.display_name, - models: entry.models, - custom_supported: entry.custom_supported, - } - } -} - -/// A list of Codex provider catalogue entries. -#[derive(Debug, Clone, Serialize)] -#[serde(transparent)] -pub struct CodexProviderListDto(pub Vec); - -impl From for CodexProviderListDto { - fn from(out: application::ListCodexProvidersOutput) -> Self { - Self(out.providers.into_iter().map(Into::into).collect()) - } -} - -/// One entry of the static Claude provider catalogue (ticket #99). -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ClaudeProviderDto { - pub provider_id: String, - pub display_name: String, - pub models: Vec, -} - -impl From for ClaudeProviderDto { - fn from(entry: application::ClaudeProviderCatalogEntry) -> Self { - Self { - provider_id: entry.provider_id, - display_name: entry.display_name, - models: entry.models, - } - } -} - -/// A list of Claude provider catalogue entries. -#[derive(Debug, Clone, Serialize)] -#[serde(transparent)] -pub struct ClaudeProviderListDto(pub Vec); - -impl From for ClaudeProviderListDto { - fn from(out: application::ListClaudeProvidersOutput) -> Self { - Self(out.providers.into_iter().map(Into::into).collect()) - } -} - /// A list of OpenCode cloud-provider catalogue entries (camelCase array on the /// wire). #[derive(Debug, Clone, Serialize)] @@ -1246,63 +1181,6 @@ impl From for ProfileDto { } } -/// Request DTO for `save_codex_provider_profile` (ticket #99). -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SaveCodexProviderProfileRequestDto { - pub profile: AgentProfile, - pub provider_id: String, - pub model: String, - pub api_key: String, - #[serde(default)] - pub custom: Option, -} - -impl From for SaveCodexProviderProfileInput { - fn from(dto: SaveCodexProviderProfileRequestDto) -> Self { - Self { - profile: dto.profile, - provider_id: dto.provider_id, - model: dto.model, - api_key: dto.api_key, - custom: dto.custom, - } - } -} - -impl From for ProfileDto { - fn from(out: SaveCodexProviderProfileOutput) -> Self { - Self(out.profile) - } -} - -/// Request DTO for `save_claude_provider_profile` (ticket #99). -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SaveClaudeProviderProfileRequestDto { - pub profile: AgentProfile, - pub provider_id: String, - pub model: String, - pub api_key: String, -} - -impl From for SaveClaudeProviderProfileInput { - fn from(dto: SaveClaudeProviderProfileRequestDto) -> Self { - Self { - profile: dto.profile, - provider_id: dto.provider_id, - model: dto.model, - api_key: dto.api_key, - } - } -} - -impl From for ProfileDto { - fn from(out: SaveClaudeProviderProfileOutput) -> Self { - Self(out.profile) - } -} - /// Request DTO for `clone_opencode_profile_from_seed`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs index 753458d..5526b71 100644 --- a/crates/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -26,13 +26,13 @@ use application::{ GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage, GitStatus, GitUnstage, HarvestMemoryFromTurn, HealthUseCase, InspectConversation, InstallPluginFromArchive, InstallPluginFromDirectory, JsonPluginManifestValidator, LaunchAgent, LaunchAgentInput, - LinkIssues, ListAgents, ListAgentsInput, ListClaudeProviders, ListCodexProviders, ListDevices, - ListEmbedderProfiles, ListIssues, ListLayouts, ListMemories, ListModelServers, - ListOpenCodeProviders, ListPluginRuntimeContributions, ListPlugins, ListProfiles, ListProjects, - ListResumableAgents, ListSkills, ListSprints, ListTemplates, LiveAgentRegistry, LiveSessions, - LiveStateLeanProvider, LiveStateProvider, LiveStateReadProvider, LoadLayout, McpRuntime, - McpToolPermissionCatalogue, MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject, - OpenTerminal, OpenTicketAssistant, OrchestratorService, PairAttemptLimiter, PairDevice, + LinkIssues, ListAgents, ListAgentsInput, ListDevices, ListEmbedderProfiles, ListIssues, + ListLayouts, ListMemories, ListModelServers, ListOpenCodeProviders, + ListPluginRuntimeContributions, ListPlugins, ListProfiles, ListProjects, ListResumableAgents, + ListSkills, ListSprints, ListTemplates, LiveAgentRegistry, LiveSessions, LiveStateLeanProvider, + LiveStateProvider, LiveStateReadProvider, LoadLayout, McpRuntime, McpToolPermissionCatalogue, + MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject, OpenTerminal, + OpenTicketAssistant, OrchestratorService, PairAttemptLimiter, PairDevice, PermissionProjectorRegistry, ProposeContext, ReadAgentContext, ReadContext, ReadConversationPage, ReadIssue, ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory, ReadMemoryIndex, ReadProjectContext, ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts, @@ -40,17 +40,17 @@ use application::{ RecordTurnProvider, ReferenceProfiles, RenameDevice, RenameLayout, RenameSprint, ReorderSprints, ResizeTerminal, ResolveAgentPermissions, ResolveAgentSystemPermissions, ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask, ReviewPluginPackage, - RevokeAllDevices, RevokeDevice, RotateConversationLog, SaveClaudeProviderProfile, - SaveCodexProviderProfile, SaveEmbedderProfile, SaveModelServer, SaveOpenCodeProviderProfile, - SaveProfile, SessionLimitService, SetActiveLayout, SetPluginEnabled, SnapshotOpenWindows, - SnapshotRunningAgents, SpawnBackgroundCommand, StopLiveAgent, StructuredRoutingMode, - StructuredSessions, SuggestedThisSession, SyncAgentWithTemplate, TerminalSessions, TouchDevice, - UnassignSkillFromAgent, UnassignTicketFromSprint, UninstallPlugin, UnlinkIssues, - UpdateAgentContext, UpdateAgentMcpToolPermissions, UpdateAgentPermissions, - UpdateAgentSystemPermissions, UpdateIssue, UpdateIssueCarnet, UpdateLiveState, UpdateMemory, - UpdateProjectContext, UpdateProjectMcpToolPermissions, UpdateProjectPermissions, - UpdateProjectSystemPermissions, UpdateSkill, UpdateTemplate, WakeSessionProvider, WriteMemory, - WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET, + RevokeAllDevices, RevokeDevice, RotateConversationLog, SaveEmbedderProfile, SaveModelServer, + SaveOpenCodeProviderProfile, SaveProfile, SessionLimitService, SetActiveLayout, + SetPluginEnabled, SnapshotOpenWindows, SnapshotRunningAgents, SpawnBackgroundCommand, + StopLiveAgent, StructuredRoutingMode, StructuredSessions, SuggestedThisSession, + SyncAgentWithTemplate, TerminalSessions, TouchDevice, UnassignSkillFromAgent, + UnassignTicketFromSprint, UninstallPlugin, UnlinkIssues, UpdateAgentContext, + UpdateAgentMcpToolPermissions, UpdateAgentPermissions, UpdateAgentSystemPermissions, + UpdateIssue, UpdateIssueCarnet, UpdateLiveState, UpdateMemory, UpdateProjectContext, + UpdateProjectMcpToolPermissions, UpdateProjectPermissions, UpdateProjectSystemPermissions, + UpdateSkill, UpdateTemplate, WakeSessionProvider, WriteMemory, WriteToTerminal, + AGENT_MEMORY_RECALL_BUDGET, }; use async_trait::async_trait; use domain::ports::{ @@ -938,18 +938,8 @@ pub struct BackendCore { /// Save (upsert) an OpenCode profile backed by a cloud provider, sealing its /// literal API key into the [`SecretStore`] (ticket #92, lot B3). pub save_opencode_provider_profile: Arc, - /// Save (upsert) a Codex profile backed by a provider/model, sealing its - /// literal API key into the [`SecretStore`] (ticket #99). - pub save_codex_provider_profile: Arc, - /// Save (upsert) a Claude profile backed by a provider/model, sealing its - /// literal API key into the [`SecretStore`] (ticket #99). - pub save_claude_provider_profile: Arc, /// Static catalogue of OpenCode cloud providers (ticket #92, lot B3). pub list_opencode_providers: Arc, - /// Static catalogue of Codex providers (ticket #99). - pub list_codex_providers: Arc, - /// Static catalogue of Claude providers (ticket #99). - pub list_claude_providers: Arc, /// Create a new OpenCode profile instance from the canonical seed. pub clone_opencode_profile_from_seed: Arc, /// Delete a profile. @@ -1477,19 +1467,7 @@ impl BackendCore { Arc::clone(&secret_store_port), Arc::clone(&ids) as Arc, )); - let save_codex_provider_profile = Arc::new(SaveCodexProviderProfile::new( - Arc::clone(&profile_store_port), - Arc::clone(&secret_store_port), - Arc::clone(&ids) as Arc, - )); - let save_claude_provider_profile = Arc::new(SaveClaudeProviderProfile::new( - Arc::clone(&profile_store_port), - Arc::clone(&secret_store_port), - Arc::clone(&ids) as Arc, - )); let list_opencode_providers = Arc::new(ListOpenCodeProviders::new()); - let list_codex_providers = Arc::new(ListCodexProviders::new()); - let list_claude_providers = Arc::new(ListClaudeProviders::new()); let clone_opencode_profile_from_seed = Arc::new(CloneOpenCodeProfileFromSeed::new( Arc::clone(&profile_store_port), Arc::clone(&ids) as Arc, @@ -2682,11 +2660,7 @@ impl BackendCore { list_profiles, save_profile, save_opencode_provider_profile, - save_codex_provider_profile, - save_claude_provider_profile, list_opencode_providers, - list_codex_providers, - list_claude_providers, clone_opencode_profile_from_seed, delete_profile, configure_profiles, diff --git a/crates/domain/src/permission.rs b/crates/domain/src/permission.rs index 3dd98ac..d28e339 100644 --- a/crates/domain/src/permission.rs +++ b/crates/domain/src/permission.rs @@ -853,12 +853,6 @@ pub struct ProjectionContext<'a> { /// Optional model selected by the agent profile. Orthogonal to permissions: /// projectors may still materialise it even when `eff == None`. pub model: Option<&'a str>, - /// Optional Codex `model_provider` value selected by the profile. - pub model_provider: Option<&'a str>, - /// Optional base URL for a custom Codex model provider. - pub model_provider_base_url: Option<&'a str>, - /// Optional API-key environment variable name for a model provider. - pub model_provider_env_key: Option<&'a str>, } /// One file a projector wants materialised at launch, tagged by **ownership**. diff --git a/crates/domain/src/profile.rs b/crates/domain/src/profile.rs index dedf39c..d40afa3 100644 --- a/crates/domain/src/profile.rs +++ b/crates/domain/src/profile.rs @@ -423,125 +423,6 @@ impl OpenCodeProviderConfig { } } -/// Configuration additive d'un provider Codex personnalisé (endpoint -/// OpenAI-compatible arbitraire, hors providers natifs Codex). -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CodexCustomProviderConfig { - /// URL de base de l'endpoint OpenAI-compatible. - pub base_url: String, - /// Libellé optionnel écrit dans la table `model_providers`. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub display_name: Option, -} - -impl CodexCustomProviderConfig { - /// Construit une configuration validée (parse-don't-validate). - /// - /// # Errors - /// Renvoie [`DomainError::EmptyField`] si `base_url`, ou un `display_name` - /// fourni non vide après trim, est vide. - pub fn new( - base_url: impl Into, - display_name: Option, - ) -> Result { - let base_url = base_url.into(); - crate::validation::non_empty(&base_url, "codexProvider.custom.baseUrl")?; - if let Some(name) = &display_name { - crate::validation::non_empty(name, "codexProvider.custom.displayName")?; - } - Ok(Self { - base_url, - display_name, - }) - } -} - -/// Configuration déclarative d'un profil Codex contrôlé par IdeA. -/// -/// Le modèle est rendu dans le `$CODEX_HOME/config.toml` isolé du run dir, et la -/// clé API réelle reste dans le [`crate::ports::SecretStore`]. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CodexProviderConfig { - /// Identifiant du provider Codex (`"openai"` ou un provider custom). - pub provider_id: String, - /// Nom du modèle servi par ce provider. - pub model: String, - /// Référence opaque vers la clé API réelle, jamais persistée en clair. - pub api_key_ref: crate::ports::SecretRef, - /// Configuration additive d'un provider personnalisé. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub custom: Option, -} - -impl CodexProviderConfig { - /// Construit une configuration validée. - /// - /// # Errors - /// Renvoie [`DomainError::EmptyField`] si `provider_id` ou `model` est vide. - pub fn new( - provider_id: impl Into, - model: impl Into, - api_key_ref: crate::ports::SecretRef, - ) -> Result { - let provider_id = provider_id.into(); - let model = model.into(); - crate::validation::non_empty(&provider_id, "codexProvider.providerId")?; - crate::validation::non_empty(&model, "codexProvider.model")?; - Ok(Self { - provider_id, - model, - api_key_ref, - custom: None, - }) - } - - /// Attache une configuration de provider personnalisé. - #[must_use] - pub fn with_custom(mut self, custom: CodexCustomProviderConfig) -> Self { - self.custom = Some(custom); - self - } -} - -/// Configuration déclarative d'un profil Claude contrôlé par IdeA. -/// -/// Le modèle est rendu dans le `.claude/settings.local.json` isolé du run dir, et -/// la clé API réelle reste dans le [`crate::ports::SecretStore`]. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ClaudeProviderConfig { - /// Identifiant du provider Claude. V1 backend: `"anthropic"`. - pub provider_id: String, - /// Nom du modèle Claude à poser au lancement. - pub model: String, - /// Référence opaque vers la clé API réelle, jamais persistée en clair. - pub api_key_ref: crate::ports::SecretRef, -} - -impl ClaudeProviderConfig { - /// Construit une configuration validée. - /// - /// # Errors - /// Renvoie [`DomainError::EmptyField`] si `provider_id` ou `model` est vide. - pub fn new( - provider_id: impl Into, - model: impl Into, - api_key_ref: crate::ports::SecretRef, - ) -> Result { - let provider_id = provider_id.into(); - let model = model.into(); - crate::validation::non_empty(&provider_id, "claudeProvider.providerId")?; - crate::validation::non_empty(&model, "claudeProvider.model")?; - Ok(Self { - provider_id, - model, - api_key_ref, - }) - } -} - /// Configuration additive d'un provider OpenCode **personnalisé** (endpoint /// OpenAI-compatible arbitraire, hors catalogue OpenCode), portée par /// [`OpenCodeProviderConfig::custom`]. @@ -1015,16 +896,11 @@ pub struct AgentProfile { /// avant. #[serde(default, skip_serializing_if = "Option::is_none")] pub opencode_provider: Option, - /// Configuration Codex provider/modèle (ticket #99). `None` pour les profils - /// non-Codex et les profils Codex historiques qui gardent le défaut natif de - /// la CLI. + /// Modèle explicitement configuré pour les CLIs qui supportent un réglage + /// déclaratif direct (Codex/Claude). `None` conserve le défaut natif de la + /// CLI. OpenCode garde ses champs dédiés. #[serde(default, skip_serializing_if = "Option::is_none")] - pub codex_provider: Option, - /// Configuration Claude provider/modèle (ticket #99). `None` pour les profils - /// non-Claude et les profils Claude historiques qui gardent le défaut natif de - /// la CLI. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub claude_provider: Option, + pub model: Option, /// Capacité **MCP** (ARCHITECTURE §14.3, orchestration v3, Décision 1). /// `None` ⇒ repli fichier `.ideai/requests` + prose (comportement actuel). /// `Some(_)` ⇒ IdeA matérialise la config MCP de cette CLI au lancement et @@ -1231,8 +1107,7 @@ impl AgentProfile { chat_http: None, opencode: None, opencode_provider: None, - codex_provider: None, - claude_provider: None, + model: None, mcp: None, liveness: None, rate_limit_pattern: None, @@ -1283,17 +1158,10 @@ impl AgentProfile { self } - /// Builder : fixe la configuration Codex provider/modèle (ticket #99). + /// Builder : fixe le modèle CLI direct (Codex/Claude, ticket #99). #[must_use] - pub fn with_codex_provider(mut self, config: CodexProviderConfig) -> Self { - self.codex_provider = Some(config); - self - } - - /// Builder : fixe la configuration Claude provider/modèle (ticket #99). - #[must_use] - pub fn with_claude_provider(mut self, config: ClaudeProviderConfig) -> Self { - self.claude_provider = Some(config); + pub fn with_model(mut self, model: impl Into) -> Self { + self.model = Some(model.into()); self } @@ -1608,50 +1476,20 @@ mod mcp_tests { } #[test] - fn codex_and_claude_provider_configs_round_trip_camelcase() { - let codex = CodexProviderConfig::new( - "openai", - "gpt-5", - crate::ports::SecretRef::new("secret-openai"), - ) - .unwrap() - .with_custom( - CodexCustomProviderConfig::new( - "https://models.example.test/v1", - Some("Example".to_owned()), - ) - .unwrap(), - ); - let claude = ClaudeProviderConfig::new( - "anthropic", - "claude-sonnet-4-5", - crate::ports::SecretRef::new("secret-anthropic"), - ) - .unwrap(); + fn profile_model_round_trips_without_codex_or_claude_provider_config() { let profile = profile_without_mcp() .with_structured_adapter(StructuredAdapter::Codex) - .with_codex_provider(codex.clone()) - .with_claude_provider(claude.clone()); + .with_model("gpt-5-codex"); let json = serde_json::to_string(&profile).expect("serialise"); - assert!(json.contains("\"codexProvider\""), "got: {json}"); - assert!(json.contains("\"claudeProvider\""), "got: {json}"); - assert!(json.contains("\"providerId\":\"openai\""), "got: {json}"); - assert!(json.contains("\"baseUrl\""), "got: {json}"); + assert!(json.contains("\"model\":\"gpt-5-codex\""), "got: {json}"); + assert!(!json.contains("codexProvider"), "got: {json}"); + assert!(!json.contains("claudeProvider"), "got: {json}"); + assert!(!json.contains("apiKeyRef"), "got: {json}"); + assert!(!json.contains("providerId"), "got: {json}"); let back: AgentProfile = serde_json::from_str(&json).expect("deserialise"); - assert_eq!(back.codex_provider, Some(codex)); - assert_eq!(back.claude_provider, Some(claude)); - } - - #[test] - fn codex_and_claude_provider_configs_reject_empty_fields() { - let secret_ref = crate::ports::SecretRef::new("secret-1"); - assert!(CodexProviderConfig::new("", "gpt-5", secret_ref.clone()).is_err()); - assert!(CodexProviderConfig::new("openai", "", secret_ref.clone()).is_err()); - assert!(ClaudeProviderConfig::new("", "claude-sonnet-4-5", secret_ref.clone()).is_err()); - assert!(ClaudeProviderConfig::new("anthropic", "", secret_ref).is_err()); - assert!(CodexCustomProviderConfig::new("", None).is_err()); + assert_eq!(back.model.as_deref(), Some("gpt-5-codex")); } #[test] diff --git a/crates/infrastructure/src/assistant/mod.rs b/crates/infrastructure/src/assistant/mod.rs index d526fb7..c62bd8f 100644 --- a/crates/infrastructure/src/assistant/mod.rs +++ b/crates/infrastructure/src/assistant/mod.rs @@ -235,6 +235,7 @@ impl TicketAssistantEnvironmentPreparer { &declaration, cwd.as_str(), project.root.as_str(), + profile.model.as_deref(), ); self.write_file(&path, rendered.as_bytes()).await?; env.push((home_env.clone(), parent_dir(cwd, target))); @@ -511,12 +512,13 @@ fn codex_config_toml( mcp_declaration: &str, run_dir: &str, project_root: &str, + model: Option<&str>, ) -> String { - let mut text = replace_toml_table_block( - existing.unwrap_or_default(), - "mcp_servers.idea", - mcp_declaration.trim_end(), - ); + let mut text = existing.unwrap_or_default().to_owned(); + if let Some(model) = model { + text = set_top_level_toml_value(&text, "model", model); + } + text = replace_toml_table_block(&text, "mcp_servers.idea", mcp_declaration.trim_end()); text = ensure_codex_project_trust(&text, run_dir); text = ensure_codex_project_trust(&text, project_root); if !text.ends_with('\n') { @@ -525,6 +527,33 @@ fn codex_config_toml( text } +fn set_top_level_toml_value(input: &str, key: &str, value: &str) -> String { + let line = format!("{key} = {}", toml_quoted(value)); + set_top_level_toml_line(input, key, &line) +} + +fn set_top_level_toml_line(input: &str, key: &str, replacement: &str) -> String { + let needle = format!("{key} ="); + let mut out = Vec::new(); + let mut replaced = false; + for line in input.lines() { + let trimmed = line.trim_start(); + if !replaced && trimmed.starts_with(&needle) { + out.push(replacement.to_owned()); + replaced = true; + } else { + out.push(line.to_owned()); + } + } + if !replaced { + if !out.is_empty() && !out.first().is_some_and(|line| line.trim().starts_with('[')) { + out.push(String::new()); + } + out.insert(0, replacement.to_owned()); + } + out.join("\n") +} + fn replace_toml_table_block(existing: &str, table: &str, replacement: &str) -> String { let header = format!("[{table}]"); let mut out = Vec::new(); diff --git a/crates/infrastructure/src/permission/claude.rs b/crates/infrastructure/src/permission/claude.rs index 132709f..ee95977 100644 --- a/crates/infrastructure/src/permission/claude.rs +++ b/crates/infrastructure/src/permission/claude.rs @@ -228,9 +228,6 @@ mod tests { project_root: root, run_dir, model: None, - model_provider: None, - model_provider_base_url: None, - model_provider_env_key: None, } } @@ -285,9 +282,6 @@ mod tests { project_root: "/proj", run_dir: "/run", model: Some("claude-sonnet-4-5"), - model_provider: None, - model_provider_base_url: None, - model_provider_env_key: None, }; let proj = ClaudePermissionProjector.project(None, None, &ctx); assert!(proj.args.is_empty()); diff --git a/crates/infrastructure/src/permission/codex.rs b/crates/infrastructure/src/permission/codex.rs index 71b03aa..c51ee96 100644 --- a/crates/infrastructure/src/permission/codex.rs +++ b/crates/infrastructure/src/permission/codex.rs @@ -133,49 +133,17 @@ fn codex_managed_keys(ctx: &ProjectionContext, include_permissions: bool) -> Vec if ctx.model.is_some() { keys.push("model".to_owned()); } - if ctx.model_provider.is_some() { - keys.push("model_provider".to_owned()); - } keys } -fn codex_managed_tables(ctx: &ProjectionContext) -> Vec { - let mut tables = vec![SANDBOX_WORKSPACE_WRITE_TABLE.to_owned()]; - if let Some(provider) = ctx.model_provider { - if ctx.model_provider_base_url.is_some() { - tables.push(codex_model_provider_table(provider)); - } - } - tables +fn codex_managed_tables(_ctx: &ProjectionContext) -> Vec { + vec![SANDBOX_WORKSPACE_WRITE_TABLE.to_owned()] } fn append_codex_model_config(contents: &mut String, ctx: &ProjectionContext) { if let Some(model) = ctx.model { contents.push_str(&format!("model = {}\n", toml_string(model))); } - let Some(provider) = ctx.model_provider else { - return; - }; - contents.push_str(&format!("model_provider = {}\n", toml_string(provider))); - if let Some(base_url) = ctx.model_provider_base_url { - let table = codex_model_provider_table(provider); - let name = provider_display_name(ctx); - let env_key = ctx.model_provider_env_key.unwrap_or("OPENAI_API_KEY"); - contents.push_str(&format!( - "\n[{table}]\nname = {}\nbase_url = {}\nenv_key = {}\nwire_api = \"responses\"\n", - toml_string(name), - toml_string(base_url), - toml_string(env_key), - )); - } -} - -fn provider_display_name<'a>(ctx: &'a ProjectionContext<'a>) -> &'a str { - ctx.model_provider.unwrap_or("custom") -} - -fn codex_model_provider_table(provider: &str) -> String { - format!("model_providers.{}", toml_string(provider)) } fn codex_network_env(network: Option) -> Vec<(String, String)> { @@ -221,9 +189,6 @@ mod tests { project_root: "/proj", run_dir: "/run/agent", model: None, - model_provider: None, - model_provider_base_url: None, - model_provider_env_key: None, } } @@ -273,14 +238,11 @@ mod tests { } #[test] - fn model_projection_without_permissions_writes_model_provider_and_custom_table() { + fn model_projection_without_permissions_writes_only_model() { let ctx = ProjectionContext { project_root: "/proj", run_dir: "/run/agent", model: Some("gpt-5"), - model_provider: Some("openai-compatible"), - model_provider_base_url: Some("https://models.example.test/v1"), - model_provider_env_key: Some("OPENAI_API_KEY"), }; let proj = CodexPermissionProjector.project(None, None, &ctx); assert!(proj.args.is_empty()); @@ -292,27 +254,16 @@ mod tests { .. } => { assert!(managed_keys.contains(&"model".to_owned())); - assert!(managed_keys.contains(&"model_provider".to_owned())); - assert!( - managed_tables.contains(&"model_providers.\"openai-compatible\"".to_owned()) + assert!(!managed_keys.contains(&"model_provider".to_owned())); + assert_eq!( + managed_tables, + &vec![SANDBOX_WORKSPACE_WRITE_TABLE.to_owned()] ); assert!(contents.contains("model = \"gpt-5\""), "{contents}"); - assert!( - contents.contains("model_provider = \"openai-compatible\""), - "{contents}" - ); - assert!( - contents.contains("[model_providers.\"openai-compatible\"]"), - "{contents}" - ); - assert!( - contents.contains("base_url = \"https://models.example.test/v1\""), - "{contents}" - ); - assert!( - contents.contains("env_key = \"OPENAI_API_KEY\""), - "{contents}" - ); + assert!(!contents.contains("model_provider"), "{contents}"); + assert!(!contents.contains("model_providers"), "{contents}"); + assert!(!contents.contains("base_url"), "{contents}"); + assert!(!contents.contains("env_key"), "{contents}"); } ProjectedFile::Replace { .. } => panic!("Codex must emit a MergeToml file"), } diff --git a/crates/web-server/src/lib.rs b/crates/web-server/src/lib.rs index a63b6c8..dc79106 100644 --- a/crates/web-server/src/lib.rs +++ b/crates/web-server/src/lib.rs @@ -69,29 +69,28 @@ use backend::dto::{ parse_sprint_id_dto, parse_sprint_status_dto, parse_task_id, parse_template_id, sort_ticket_rows, sprint_version_dto, update_input, version_dto, AgentDriftListDto, AgentDto, AgentListDto, AssignSkillRequestDto, AttachLiveAgentRequestDto, AttachLiveAgentResponseDto, - BackgroundTaskDto, ChangeAgentProfileDto, ChangeAgentProfileRequestDto, ClaudeProviderListDto, - CloneOpenCodeProfileFromSeedRequestDto, CodexProviderListDto, ConfigureProfilesRequestDto, - ConversationDetailsDto, CreateAgentFromTemplateRequestDto, CreateAgentRequestDto, - CreateMemoryRequestDto, CreateSkillRequestDto, CreateTemplateRequestDto, - DetectProfilesRequestDto, DetectProfilesResponseDto, EffectivePermissionsDto, - EmbedderEnginesDto, EmbedderProfileDto, EmbedderProfileListDto, ErrorDto, FirstRunStateDto, - GitBranchesDto, GitCheckoutRequestDto, GitCommitDto, GitCommitListDto, GitCommitRequestDto, - GitStageRequestDto, GitStatusListDto, GraphCommitListDto, HealthRequestDto, HealthResponseDto, - InspectConversationRequestDto, LaunchAgentRequestDto, LiveAgentListDto, MemoryDto, - MemoryIndexDto, MemoryLinksDto, MemoryListDto, OpenCodeProviderListDto, OpenTerminalRequestDto, - ProfileDto, ProfileListDto, ProjectDto, ProjectListDto, ProjectMcpToolPermissionsDto, - ProjectPermissionsDto, ProjectSystemPermissionsDto, ProjectWorkStateDto, - ReadAgentContextResponseDto, ReadConversationPageRequestDto, RecallMemoryRequestDto, - ResolveAgentPermissionsRequestDto, ResolveAgentSystemPermissionsRequestDto, - ResolvedAgentSystemPermissionsDto, ResumableAgentListDto, SaveClaudeProviderProfileRequestDto, - SaveCodexProviderProfileRequestDto, SaveEmbedderProfileRequestDto, - SaveOpenCodeProviderProfileRequestDto, SaveProfileRequestDto, SkillDto, SkillListDto, - SprintCreateRequestDto, SprintDeleteRequestDto, SprintDto, SprintListDto, SprintListRequestDto, - SprintRenameRequestDto, SprintReorderRequestDto, StopLiveAgentRequestDto, - StopLiveAgentResponseDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, - TemplateListDto, TerminalSessionDto, TicketAssignRequestDto, TicketCarnetDto, - TicketCreateRequestDto, TicketDeleteRequestDto, TicketDto, TicketLinkCommandRequestDto, - TicketListPageInput, TicketListRequestDto, TicketReadRequestDto, TicketSprintAssignRequestDto, + BackgroundTaskDto, ChangeAgentProfileDto, ChangeAgentProfileRequestDto, + CloneOpenCodeProfileFromSeedRequestDto, ConfigureProfilesRequestDto, ConversationDetailsDto, + CreateAgentFromTemplateRequestDto, CreateAgentRequestDto, CreateMemoryRequestDto, + CreateSkillRequestDto, CreateTemplateRequestDto, DetectProfilesRequestDto, + DetectProfilesResponseDto, EffectivePermissionsDto, EmbedderEnginesDto, EmbedderProfileDto, + EmbedderProfileListDto, ErrorDto, FirstRunStateDto, GitBranchesDto, GitCheckoutRequestDto, + GitCommitDto, GitCommitListDto, GitCommitRequestDto, GitStageRequestDto, GitStatusListDto, + GraphCommitListDto, HealthRequestDto, HealthResponseDto, InspectConversationRequestDto, + LaunchAgentRequestDto, LiveAgentListDto, MemoryDto, MemoryIndexDto, MemoryLinksDto, + MemoryListDto, OpenCodeProviderListDto, OpenTerminalRequestDto, ProfileDto, ProfileListDto, + ProjectDto, ProjectListDto, ProjectMcpToolPermissionsDto, ProjectPermissionsDto, + ProjectSystemPermissionsDto, ProjectWorkStateDto, ReadAgentContextResponseDto, + ReadConversationPageRequestDto, RecallMemoryRequestDto, ResolveAgentPermissionsRequestDto, + ResolveAgentSystemPermissionsRequestDto, ResolvedAgentSystemPermissionsDto, + ResumableAgentListDto, SaveEmbedderProfileRequestDto, SaveOpenCodeProviderProfileRequestDto, + SaveProfileRequestDto, SkillDto, SkillListDto, SprintCreateRequestDto, SprintDeleteRequestDto, + SprintDto, SprintListDto, SprintListRequestDto, SprintRenameRequestDto, + SprintReorderRequestDto, StopLiveAgentRequestDto, StopLiveAgentResponseDto, + SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto, + TerminalSessionDto, TicketAssignRequestDto, TicketCarnetDto, TicketCreateRequestDto, + TicketDeleteRequestDto, TicketDto, TicketLinkCommandRequestDto, TicketListPageInput, + TicketListRequestDto, TicketReadRequestDto, TicketSprintAssignRequestDto, TicketSprintUnassignRequestDto, TicketUnlinkCommandRequestDto, TicketUpdateCarnetRequestDto, TicketUpdateRequestDto, TurnPageDto, UnassignSkillRequestDto, UpdateAgentContextRequestDto, UpdateAgentMcpToolPermissionsRequestDto, UpdateAgentPermissionsRequestDto, @@ -2357,14 +2356,6 @@ async fn invoke( "save_opencode_provider_profile" => { invoke_save_opencode_provider_profile(&request.args, &state.app).await } - "list_codex_providers" => invoke_list_codex_providers(&state.app), - "save_codex_provider_profile" => { - invoke_save_codex_provider_profile(&request.args, &state.app).await - } - "list_claude_providers" => invoke_list_claude_providers(&state.app), - "save_claude_provider_profile" => { - invoke_save_claude_provider_profile(&request.args, &state.app).await - } "delete_profile" => invoke_delete_profile(&request.args, &state.app).await, "configure_profiles" => invoke_configure_profiles(&request.args, &state.app).await, "clone_opencode_profile_from_seed" => { @@ -2632,50 +2623,6 @@ async fn invoke_save_opencode_provider_profile( serde_json::to_value(output).map_err(serialization_error) } -fn invoke_list_codex_providers(state: &BackendCore) -> Result { - let output: CodexProviderListDto = state.list_codex_providers.execute().into(); - serde_json::to_value(output).map_err(serialization_error) -} - -async fn invoke_save_codex_provider_profile( - args: &Value, - state: &BackendCore, -) -> Result { - let request = required_request::( - "save_codex_provider_profile", - args, - )?; - let output = state - .save_codex_provider_profile - .execute(request.into()) - .await - .map(ProfileDto::from) - .map_err(ErrorDto::from)?; - serde_json::to_value(output).map_err(serialization_error) -} - -fn invoke_list_claude_providers(state: &BackendCore) -> Result { - let output: ClaudeProviderListDto = state.list_claude_providers.execute().into(); - serde_json::to_value(output).map_err(serialization_error) -} - -async fn invoke_save_claude_provider_profile( - args: &Value, - state: &BackendCore, -) -> Result { - let request = required_request::( - "save_claude_provider_profile", - args, - )?; - let output = state - .save_claude_provider_profile - .execute(request.into()) - .await - .map(ProfileDto::from) - .map_err(ErrorDto::from)?; - serde_json::to_value(output).map_err(serialization_error) -} - async fn invoke_delete_profile(args: &Value, state: &BackendCore) -> Result { let profile_id = string_arg(args, "profileId", "delete_profile")?; let input = backend::dto::parse_delete_profile(profile_id)?; @@ -7707,10 +7654,6 @@ mod tests { "save_profile", "list_opencode_providers", "save_opencode_provider_profile", - "list_codex_providers", - "save_codex_provider_profile", - "list_claude_providers", - "save_claude_provider_profile", "delete_profile", "configure_profiles", "clone_opencode_profile_from_seed", diff --git a/frontend/src/adapters/http/requestResponseGateways.ts b/frontend/src/adapters/http/requestResponseGateways.ts index cc1ad51..c2c7dfc 100644 --- a/frontend/src/adapters/http/requestResponseGateways.ts +++ b/frontend/src/adapters/http/requestResponseGateways.ts @@ -15,8 +15,6 @@ import type { Agent, AgentDrift, AgentProfile, - ClaudeProviderCatalogEntry, - CodexProviderCatalogEntry, EffectivePermissions, EmbedderEngines, EmbedderProfile, @@ -67,8 +65,6 @@ import type { PermissionGateway, ProfileGateway, ProjectGateway, - SaveClaudeProviderProfileInput, - SaveCodexProviderProfileInput, SaveOpenCodeProviderProfileInput, SkillGateway, TemplateGateway, @@ -206,37 +202,6 @@ export class HttpProfileGateway implements ProfileGateway { }, }); } - listCodexProviders(): Promise { - return this.http.invoke("list_codex_providers"); - } - saveCodexProviderProfile( - input: SaveCodexProviderProfileInput, - ): Promise { - return this.http.invoke("save_codex_provider_profile", { - request: { - profile: input.profile, - providerId: input.providerId, - model: input.model, - apiKey: input.apiKey, - custom: input.custom, - }, - }); - } - listClaudeProviders(): Promise { - return this.http.invoke("list_claude_providers"); - } - saveClaudeProviderProfile( - input: SaveClaudeProviderProfileInput, - ): Promise { - return this.http.invoke("save_claude_provider_profile", { - request: { - profile: input.profile, - providerId: input.providerId, - model: input.model, - apiKey: input.apiKey, - }, - }); - } } export class HttpModelServerGateway implements ModelServerGateway { diff --git a/frontend/src/adapters/mock/index.ts b/frontend/src/adapters/mock/index.ts index 7123f9d..153a036 100644 --- a/frontend/src/adapters/mock/index.ts +++ b/frontend/src/adapters/mock/index.ts @@ -9,8 +9,6 @@ import type { AgentDrift, AppExitWorkGuardState, AgentProfile, - ClaudeProviderCatalogEntry, - CodexProviderCatalogEntry, DiagnosticWarning, DomainEvent, EmbedderEngines, @@ -105,8 +103,6 @@ import type { ReattachResult, RemoteGateway, ReviewPluginPackageInput, - SaveClaudeProviderProfileInput, - SaveCodexProviderProfileInput, SaveOpenCodeProviderProfileInput, SkillGateway, StoppedLiveAgent, @@ -1297,25 +1293,6 @@ const MOCK_OPENCODE_PROVIDERS: OpenCodeProviderCatalogEntry[] = [ }, ]; -/** Static mock catalogue mirroring the backend Codex provider list. */ -const MOCK_CODEX_PROVIDERS: CodexProviderCatalogEntry[] = [ - { - providerId: "openai", - displayName: "OpenAI", - models: ["gpt-5", "gpt-5-mini", "gpt-5-codex", "o3"], - customSupported: true, - }, -]; - -/** Static mock catalogue mirroring the backend Claude provider list. */ -const MOCK_CLAUDE_PROVIDERS: ClaudeProviderCatalogEntry[] = [ - { - providerId: "anthropic", - displayName: "Anthropic", - models: ["claude-sonnet-4-5", "claude-opus-4-1", "claude-haiku-3-5"], - }, -]; - /** * In-memory profiles gateway. Tracks configured profiles and a first-run flag so * the wizard can be driven and tested fully offline. By default it reports the @@ -1412,52 +1389,6 @@ export class MockProfileGateway implements ProfileGateway { return structuredClone(saved); } - async listCodexProviders(): Promise { - return structuredClone(MOCK_CODEX_PROVIDERS); - } - - async saveCodexProviderProfile( - input: SaveCodexProviderProfileInput, - ): Promise { - const saved: AgentProfile = { - ...structuredClone(input.profile), - codexProvider: { - providerId: input.providerId, - model: input.model, - apiKeyRef: - input.profile.codexProvider?.apiKeyRef ?? `mock-secret-${input.profile.id}`, - custom: input.custom, - }, - }; - const i = this.profiles.findIndex((p) => p.id === saved.id); - if (i >= 0) this.profiles[i] = saved; - else this.profiles.push(saved); - this.configured = true; - return structuredClone(saved); - } - - async listClaudeProviders(): Promise { - return structuredClone(MOCK_CLAUDE_PROVIDERS); - } - - async saveClaudeProviderProfile( - input: SaveClaudeProviderProfileInput, - ): Promise { - const saved: AgentProfile = { - ...structuredClone(input.profile), - claudeProvider: { - providerId: input.providerId, - model: input.model, - apiKeyRef: - input.profile.claudeProvider?.apiKeyRef ?? `mock-secret-${input.profile.id}`, - }, - }; - const i = this.profiles.findIndex((p) => p.id === saved.id); - if (i >= 0) this.profiles[i] = saved; - else this.profiles.push(saved); - this.configured = true; - return structuredClone(saved); - } } /** diff --git a/frontend/src/adapters/profile.ts b/frontend/src/adapters/profile.ts index b13b2f5..ea355ed 100644 --- a/frontend/src/adapters/profile.ts +++ b/frontend/src/adapters/profile.ts @@ -10,8 +10,6 @@ import { invoke } from "@tauri-apps/api/core"; import type { AgentProfile, - ClaudeProviderCatalogEntry, - CodexProviderCatalogEntry, FirstRunState, OpenCodeProviderCatalogEntry, ProfileAvailability, @@ -19,8 +17,6 @@ import type { import type { CloneOpenCodeProfileFromSeedInput, ProfileGateway, - SaveClaudeProviderProfileInput, - SaveCodexProviderProfileInput, SaveOpenCodeProviderProfileInput, } from "@/ports"; @@ -83,38 +79,4 @@ export class TauriProfileGateway implements ProfileGateway { }); } - listCodexProviders(): Promise { - return invoke("list_codex_providers"); - } - - saveCodexProviderProfile( - input: SaveCodexProviderProfileInput, - ): Promise { - return invoke("save_codex_provider_profile", { - request: { - profile: input.profile, - providerId: input.providerId, - model: input.model, - apiKey: input.apiKey, - custom: input.custom, - }, - }); - } - - listClaudeProviders(): Promise { - return invoke("list_claude_providers"); - } - - saveClaudeProviderProfile( - input: SaveClaudeProviderProfileInput, - ): Promise { - return invoke("save_claude_provider_profile", { - request: { - profile: input.profile, - providerId: input.providerId, - model: input.model, - apiKey: input.apiKey, - }, - }); - } } diff --git a/frontend/src/domain/index.ts b/frontend/src/domain/index.ts index 1a65f20..936164a 100644 --- a/frontend/src/domain/index.ts +++ b/frontend/src/domain/index.ts @@ -1068,36 +1068,6 @@ export interface OpenCodeProviderConfig { custom?: CustomProviderConfig; } -/** - * Configuration for a Codex profile backed by a provider/model pair (ticket - * #99). `apiKeyRef` is an opaque backend SecretStore reference; the literal - * secret is only sent through {@link ProfileGateway.saveCodexProviderProfile}. - */ -export interface CodexProviderConfig { - /** Provider id used as Codex's `model_provider` (e.g. `"openai"`). */ - providerId: string; - /** Model name written into Codex's isolated config. */ - model: string; - /** Opaque reference to the sealed API key; never the literal key. */ - apiKeyRef: string; - /** Optional custom OpenAI-compatible endpoint for this Codex provider. */ - custom?: CodexCustomProviderConfig; -} - -/** - * Configuration for a Claude profile backed by a provider/model pair (ticket - * #99). `apiKeyRef` is an opaque backend SecretStore reference; the literal - * secret is only sent through {@link ProfileGateway.saveClaudeProviderProfile}. - */ -export interface ClaudeProviderConfig { - /** Provider id. V1 backend exposes `"anthropic"`. */ - providerId: string; - /** Model name written into Claude's isolated settings. */ - model: string; - /** Opaque reference to the sealed API key; never the literal key. */ - apiKeyRef: string; -} - /** * Config for a custom OpenCode provider (mirror of the backend * `CustomProviderConfig`, camelCase wire format), carried by @@ -1112,18 +1082,6 @@ export interface CustomProviderConfig { displayName?: string; } -/** - * Config for a custom Codex provider (mirror of the backend - * `CodexCustomProviderConfig`, camelCase wire format), carried by - * {@link CodexProviderConfig.custom}. - */ -export interface CodexCustomProviderConfig { - /** Base URL of the OpenAI-compatible endpoint. */ - baseUrl: string; - /** Optional display label written into Codex's provider table. */ - displayName?: string; -} - /** * One entry of the static OpenCode cloud-provider catalogue (mirror of the * backend `OpenCodeProviderDto`), returned by @@ -1138,28 +1096,6 @@ export interface OpenCodeProviderCatalogEntry { models: string[]; } -/** One entry of the static Codex provider catalogue (ticket #99). */ -export interface CodexProviderCatalogEntry { - /** Provider id used as Codex's `model_provider`. */ - providerId: string; - /** Human-readable label for the picker UI. */ - displayName: string; - /** Model names this provider serves, offered for selection. */ - models: string[]; - /** Whether this provider supports a custom endpoint in the UI. */ - customSupported: boolean; -} - -/** One entry of the static Claude provider catalogue (ticket #99). */ -export interface ClaudeProviderCatalogEntry { - /** Provider id. V1 backend exposes `"anthropic"`. */ - providerId: string; - /** Human-readable label for the picker UI. */ - displayName: string; - /** Model names this provider serves, offered for selection. */ - models: string[]; -} - /** * A declarative AI-CLI profile (mirror of the backend `AgentProfile`). `id` is a * UUID string; `detect` is the optional detection command line. @@ -1196,10 +1132,11 @@ export interface AgentProfile { * both. */ opencodeProvider?: OpenCodeProviderConfig; - /** Codex provider/model config (ticket #99). */ - codexProvider?: CodexProviderConfig; - /** Claude provider/model config (ticket #99). */ - claudeProvider?: ClaudeProviderConfig; + /** + * Optional direct CLI model setting for Codex/Claude. `undefined` keeps the + * CLI's own default. OpenCode keeps its dedicated provider/local model fields. + */ + model?: string; } /** Availability of a candidate profile after detection (mirror of the DTO). */ diff --git a/frontend/src/features/first-run/FirstRunWizard.test.tsx b/frontend/src/features/first-run/FirstRunWizard.test.tsx index ce1c861..53563d5 100644 --- a/frontend/src/features/first-run/FirstRunWizard.test.tsx +++ b/frontend/src/features/first-run/FirstRunWizard.test.tsx @@ -183,47 +183,41 @@ describe("FirstRunWizard (with MockProfileGateway)", () => { }); }); -describe("FirstRunWizard — Codex/Claude provider configuration (ticket #99)", () => { - it("saves a Codex provider profile and clears the literal key", async () => { +describe("FirstRunWizard — Codex/Claude model configuration (ticket #99)", () => { + it("shows only a free-form model field for Codex and persists no provider/API key", async () => { const { profile } = renderWizard(); await waitForLoaded(); + await waitFor(() => + expect( + (screen.getByLabelText("use Claude Code") as HTMLInputElement).checked, + ).toBe(true), + ); fireEvent.click(screen.getByLabelText("use OpenAI Codex CLI")); const row = within( screen.getByLabelText("use OpenAI Codex CLI").closest("li")!, ); - const providerSelect = await row.findByLabelText("OpenAI Codex CLI provider"); - const modelSelect = row.getByLabelText( - "OpenAI Codex CLI model", - ) as HTMLSelectElement; - expect(modelSelect.disabled).toBe(true); + expect(row.queryByLabelText("OpenAI Codex CLI provider")).toBeNull(); + expect(row.queryByLabelText("OpenAI Codex CLI provider search")).toBeNull(); + expect(row.queryByLabelText("OpenAI Codex CLI api key")).toBeNull(); - fireEvent.change(providerSelect, { target: { value: "openai" } }); - expect(modelSelect.value).toBe(""); - expect(modelSelect.disabled).toBe(false); - fireEvent.change(modelSelect, { target: { value: "gpt-5-codex" } }); - fireEvent.change(row.getByLabelText("OpenAI Codex CLI api key"), { - target: { value: "sk-codex-secret" }, + fireEvent.change(row.getByLabelText("OpenAI Codex CLI model"), { + target: { value: "gpt-5-codex" }, }); - fireEvent.click(row.getByRole("button", { name: "Enregistrer Codex" })); + fireEvent.click(screen.getByRole("button", { name: "Save and continue" })); await waitFor(async () => { const saved = await profile.listProfiles(); const codex = saved.find((p) => p.command === "codex"); - expect(codex?.codexProvider).toEqual({ - providerId: "openai", - model: "gpt-5-codex", - apiKeyRef: "mock-secret-mock-codex", - custom: undefined, - }); - expect(JSON.stringify(codex)).not.toContain("sk-codex-secret"); + expect(codex?.model).toBe("gpt-5-codex"); + expect(JSON.stringify(codex)).not.toContain("provider"); + expect(JSON.stringify(codex)).not.toContain("apiKey"); }); - expect((row.getByLabelText("OpenAI Codex CLI api key") as HTMLInputElement).value).toBe(""); }); - it("saves a Claude provider profile without exposing the literal key", async () => { + it("shows only a free-form model field for Claude and persists no provider/API key", async () => { const { profile } = renderWizard(); await waitForLoaded(); @@ -233,31 +227,26 @@ describe("FirstRunWizard — Codex/Claude provider configuration (ticket #99)", if (!claudeToggle.checked) fireEvent.click(claudeToggle); const row = within(claudeToggle.closest("li")!); - fireEvent.change(await row.findByLabelText("Claude Code provider"), { - target: { value: "anthropic" }, - }); + expect(row.queryByLabelText("Claude Code provider")).toBeNull(); + expect(row.queryByLabelText("Claude Code provider search")).toBeNull(); + expect(row.queryByLabelText("Claude Code api key")).toBeNull(); + fireEvent.change(row.getByLabelText("Claude Code model"), { target: { value: "claude-sonnet-4-5" }, }); - const apiKey = row.getByLabelText("Claude Code api key") as HTMLInputElement; - fireEvent.change(apiKey, { target: { value: "sk-claude-secret" } }); - fireEvent.click(row.getByRole("button", { name: "Enregistrer Claude" })); + fireEvent.click(screen.getByRole("button", { name: "Save and continue" })); await waitFor(async () => { const saved = await profile.listProfiles(); const claude = saved.find((p) => p.command === "claude"); - expect(claude?.claudeProvider).toEqual({ - providerId: "anthropic", - model: "claude-sonnet-4-5", - apiKeyRef: "mock-secret-mock-claude", - }); - expect(JSON.stringify(claude)).not.toContain("sk-claude-secret"); + expect(claude?.model).toBe("claude-sonnet-4-5"); + expect(JSON.stringify(claude)).not.toContain("provider"); + expect(JSON.stringify(claude)).not.toContain("apiKey"); }); - expect(apiKey.value).toBe(""); }); - it("keeps an existing Codex SecretRef when editing provider/model", async () => { + it("edits an existing Codex model without rendering provider/API key fields", async () => { const profile = new MockProfileGateway(); await profile.configureProfiles([ { @@ -269,11 +258,7 @@ describe("FirstRunWizard — Codex/Claude provider configuration (ticket #99)", detect: "codex --version", cwdTemplate: "{projectRoot}", structuredAdapter: "codex", - codexProvider: { - providerId: "openai", - model: "gpt-5-mini", - apiKeyRef: "existing-secret-ref", - }, + model: "gpt-5-mini", }, ]); const gateways = { @@ -290,20 +275,22 @@ describe("FirstRunWizard — Codex/Claude provider configuration (ticket #99)", const row = within( screen.getByLabelText("use Codex configured").closest("li")!, ); - expect((row.getByLabelText("Codex configured api key") as HTMLInputElement).value).toBe(""); + expect(row.queryByLabelText("Codex configured provider")).toBeNull(); + expect(row.queryByLabelText("Codex configured api key")).toBeNull(); + expect( + (row.getByLabelText("Codex configured model") as HTMLInputElement).value, + ).toBe("gpt-5-mini"); + fireEvent.change(row.getByLabelText("Codex configured model"), { target: { value: "gpt-5" }, }); - fireEvent.change(row.getByLabelText("Codex configured api key"), { - target: { value: "sk-new-secret" }, - }); - fireEvent.click(row.getByRole("button", { name: "Enregistrer Codex" })); + fireEvent.click(screen.getByRole("button", { name: "Save and continue" })); await waitFor(async () => { const [saved] = await profile.listProfiles(); - expect(saved.codexProvider?.model).toBe("gpt-5"); - expect(saved.codexProvider?.apiKeyRef).toBe("existing-secret-ref"); - expect(JSON.stringify(saved)).not.toContain("sk-new-secret"); + expect(saved.model).toBe("gpt-5"); + expect(JSON.stringify(saved)).not.toContain("provider"); + expect(JSON.stringify(saved)).not.toContain("apiKey"); }); }); }); diff --git a/frontend/src/features/first-run/FirstRunWizard.tsx b/frontend/src/features/first-run/FirstRunWizard.tsx index 518aecf..0c95f8a 100644 --- a/frontend/src/features/first-run/FirstRunWizard.tsx +++ b/frontend/src/features/first-run/FirstRunWizard.tsx @@ -19,9 +19,6 @@ import { useCallback, useEffect, useState } from "react"; import type { AgentProfile, - ClaudeProviderCatalogEntry, - CodexCustomProviderConfig, - CodexProviderCatalogEntry, GatewayError, HttpChatConfig, LocalModelServerConfig, @@ -64,20 +61,6 @@ interface OpenCodeProviderCatalog { reload: () => void; } -interface CodexProviderCatalog { - providers: CodexProviderCatalogEntry[] | null; - loading: boolean; - error: string | null; - reload: () => void; -} - -interface ClaudeProviderCatalog { - providers: ClaudeProviderCatalogEntry[] | null; - loading: boolean; - error: string | null; - reload: () => void; -} - /** * Loads the static OpenCode cloud-provider catalogue once for the whole * wizard (every Cloud row shares it), so the provider/model pickers can be @@ -111,62 +94,6 @@ function useOpenCodeProviderCatalog(): OpenCodeProviderCatalog { return { providers, loading, error, reload: () => void load() }; } -function useCodexProviderCatalog(): CodexProviderCatalog { - const { profile } = useGateways(); - const [providers, setProviders] = useState( - null, - ); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const load = useCallback(async () => { - setLoading(true); - setError(null); - try { - setProviders(await profile.listCodexProviders()); - } catch (e) { - setProviders(null); - setError(describeError(e)); - } finally { - setLoading(false); - } - }, [profile]); - - useEffect(() => { - void load(); - }, [load]); - - return { providers, loading, error, reload: () => void load() }; -} - -function useClaudeProviderCatalog(): ClaudeProviderCatalog { - const { profile } = useGateways(); - const [providers, setProviders] = useState( - null, - ); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const load = useCallback(async () => { - setLoading(true); - setError(null); - try { - setProviders(await profile.listClaudeProviders()); - } catch (e) { - setProviders(null); - setError(describeError(e)); - } finally { - setLoading(false); - } - }, [profile]); - - useEffect(() => { - void load(); - }, [load]); - - return { providers, loading, error, reload: () => void load() }; -} - /** * Renders the wizard when it is the first run. Calls `onDone` once the user * finishes (so the host can drop the wizard and show the normal UI). Returns @@ -189,8 +116,6 @@ export function FirstRunWizard({ const vm = useFirstRun(forceOpen ? "edit" : "firstRun"); const modelServers = useModelServers(); const providerCatalog = useOpenCodeProviderCatalog(); - const codexProviderCatalog = useCodexProviderCatalog(); - const claudeProviderCatalog = useClaudeProviderCatalog(); if (vm.isFirstRun === null) return null; if (!forceOpen && vm.isFirstRun === false) return null; @@ -259,8 +184,6 @@ export function FirstRunWizard({ entry={entry} servers={modelServers.servers} providerCatalog={providerCatalog} - codexProviderCatalog={codexProviderCatalog} - claudeProviderCatalog={claudeProviderCatalog} onToggle={() => vm.toggle(entry.profile.id)} onChange={(p) => vm.updateProfile(entry.profile.id, p)} onRemove={() => vm.remove(entry.profile.id)} @@ -292,8 +215,6 @@ function ProfileRow({ entry, servers, providerCatalog, - codexProviderCatalog, - claudeProviderCatalog, onToggle, onChange, onRemove, @@ -304,8 +225,6 @@ function ProfileRow({ servers: LocalModelServerConfig[]; /** OpenCode cloud-provider catalogue (ticket #92), shared across rows. */ providerCatalog: OpenCodeProviderCatalog; - codexProviderCatalog: CodexProviderCatalog; - claudeProviderCatalog: ClaudeProviderCatalog; onToggle: () => void; onChange: (p: AgentProfile) => void; onRemove: () => void; @@ -411,22 +330,10 @@ function ProfileRow({ /> )} - {profile.structuredAdapter === "codex" && - (selected || profile.codexProvider) && ( - - )} - - {profile.structuredAdapter === "claude" && - (selected || profile.claudeProvider) && ( - + {(profile.structuredAdapter === "codex" || + profile.structuredAdapter === "claude") && + (selected || profile.model) && ( + )} ); @@ -893,430 +800,32 @@ function OpenCodeProviderFields({ ); } -type SimpleProviderCatalogEntry = - | CodexProviderCatalogEntry - | ClaudeProviderCatalogEntry; - -interface ProviderModelSecretFieldsProps { - engine: "Codex" | "Claude"; - profile: AgentProfile; - catalog: { - providers: SimpleProviderCatalogEntry[] | null; - loading: boolean; - error: string | null; - reload: () => void; - }; - existing: - | AgentProfile["codexProvider"] - | AgentProfile["claudeProvider"] - | undefined; - customSupported: boolean; - saveProfile: (input: { - providerId: string; - model: string; - apiKey: string; - custom?: CodexCustomProviderConfig; - }) => Promise; - onChange: (p: AgentProfile) => void; -} - -function ProviderModelSecretFields({ - engine, +function CliModelField({ profile, - catalog, - existing, - customSupported, - saveProfile, - onChange, -}: ProviderModelSecretFieldsProps) { - const [mode, setMode] = useState<"catalog" | "custom">( - existing && "custom" in existing && existing.custom ? "custom" : "catalog", - ); - const [providerId, setProviderId] = useState(existing?.providerId ?? ""); - const [model, setModel] = useState(existing?.model ?? ""); - const [providerFilter, setProviderFilter] = useState(""); - const existingCustom = - existing && "custom" in existing ? existing.custom : undefined; - const [customBaseUrl, setCustomBaseUrl] = useState(existingCustom?.baseUrl ?? ""); - const [customDisplayName, setCustomDisplayName] = useState( - existingCustom?.displayName ?? "", - ); - const [apiKey, setApiKey] = useState(""); - const [showKey, setShowKey] = useState(false); - const [saving, setSaving] = useState(false); - const [saveError, setSaveError] = useState(null); - const [fieldErrors, setFieldErrors] = useState({}); - - const isEditing = existing !== undefined; - const catalogReady = catalog.providers !== null && !catalog.loading; - const models = - catalog.providers?.find((p) => p.providerId === providerId)?.models ?? []; - const filteredProviders = (catalog.providers ?? []).filter((p) => { - if (p.providerId === providerId) return true; - const q = providerFilter.trim().toLowerCase(); - if (q.length === 0) return true; - return ( - p.displayName.toLowerCase().includes(q) || - p.providerId.toLowerCase().includes(q) - ); - }); - const saveDisabled = - saving || - apiKey.length === 0 || - (mode === "catalog" && (!catalogReady || Boolean(catalog.error))); - - async function save() { - const errors: CloudFieldErrors = {}; - if (providerId.trim().length === 0) { - errors.providerId = "Le provider est obligatoire."; - } - if (model.trim().length === 0) errors.model = "Le modèle est obligatoire."; - if (mode === "custom" && customBaseUrl.trim().length === 0) { - errors.baseUrl = "L'URL de base est obligatoire."; - } - if (apiKey.length === 0) errors.apiKey = "La clé API est obligatoire."; - setFieldErrors(errors); - if (Object.keys(errors).length > 0) return; - - setSaving(true); - setSaveError(null); - try { - const saved = await saveProfile({ - providerId: providerId.trim(), - model: model.trim(), - apiKey, - ...(mode === "custom" - ? { - custom: { - baseUrl: customBaseUrl.trim(), - displayName: - customDisplayName.trim().length > 0 - ? customDisplayName.trim() - : undefined, - }, - } - : {}), - }); - onChange(saved); - setApiKey(""); - } catch (e) { - setSaveError(describeError(e)); - } finally { - setSaving(false); - } - } - - return ( -
- - Provider cloud ({engine}) - - - {saveError && ( -

- {saveError} -

- )} - - {catalog.loading && ( -

Chargement des providers…

- )} - {catalog.error && ( -
-

- Impossible de charger la liste des providers cloud. -

- -
- )} - - {mode === "catalog" && ( - <> - - - - - )} - - {mode === "custom" && customSupported && ( -
- - Provider personnalisé - - - - - - - - - - - -
- )} - - - - -
- ); -} - -function CodexProviderFields({ - profile, - catalog, onChange, }: { profile: AgentProfile; - catalog: CodexProviderCatalog; onChange: (p: AgentProfile) => void; }) { - const { profile: profileGateway } = useGateways(); return ( - p.customSupported) ?? false} - saveProfile={(input) => - profileGateway.saveCodexProviderProfile({ - profile, - providerId: input.providerId, - model: input.model, - apiKey: input.apiKey, - custom: input.custom, - }) - } - onChange={onChange} - /> - ); -} - -function ClaudeProviderFields({ - profile, - catalog, - onChange, -}: { - profile: AgentProfile; - catalog: ClaudeProviderCatalog; - onChange: (p: AgentProfile) => void; -}) { - const { profile: profileGateway } = useGateways(); - return ( - - profileGateway.saveClaudeProviderProfile({ - profile, - providerId: input.providerId, - model: input.model, - apiKey: input.apiKey, - }) - } - onChange={onChange} - /> + ); } diff --git a/frontend/src/ports/index.ts b/frontend/src/ports/index.ts index 95ae44d..790742c 100644 --- a/frontend/src/ports/index.ts +++ b/frontend/src/ports/index.ts @@ -13,9 +13,6 @@ import type { AgentDrift, AgentProfile, AppExitWorkGuardState, - ClaudeProviderCatalogEntry, - CodexCustomProviderConfig, - CodexProviderCatalogEntry, CustomProviderConfig, DomainEvent, EmbedderEngines, @@ -694,24 +691,6 @@ export interface ProfileGateway { saveOpenCodeProviderProfile( input: SaveOpenCodeProviderProfileInput, ): Promise; - /** Static catalogue of Codex providers/models (ticket #99). */ - listCodexProviders(): Promise; - /** - * Creates or replaces (by id) a Codex profile with provider/model/secret - * config. The literal API key is sealed backend-side and never returned. - */ - saveCodexProviderProfile( - input: SaveCodexProviderProfileInput, - ): Promise; - /** Static catalogue of Claude providers/models (ticket #99). */ - listClaudeProviders(): Promise; - /** - * Creates or replaces (by id) a Claude profile with provider/model/secret - * config. The literal API key is sealed backend-side and never returned. - */ - saveClaudeProviderProfile( - input: SaveClaudeProviderProfileInput, - ): Promise; } /** Input for {@link ProfileGateway.cloneOpenCodeProfileFromSeed}. */ @@ -740,32 +719,6 @@ export interface SaveOpenCodeProviderProfileInput { custom?: CustomProviderConfig; } -/** Input for {@link ProfileGateway.saveCodexProviderProfile}. */ -export interface SaveCodexProviderProfileInput { - /** The profile to create or replace (by id). */ - profile: AgentProfile; - /** Provider id used as Codex's `model_provider`. */ - providerId: string; - /** Model name served by this provider. */ - model: string; - /** Literal API key — sealed into the `SecretStore`, never persisted as-is. */ - apiKey: string; - /** Optional custom endpoint config for a Codex provider. */ - custom?: CodexCustomProviderConfig; -} - -/** Input for {@link ProfileGateway.saveClaudeProviderProfile}. */ -export interface SaveClaudeProviderProfileInput { - /** The profile to create or replace (by id). */ - profile: AgentProfile; - /** Provider id. V1 backend exposes `"anthropic"`. */ - providerId: string; - /** Model name served by this provider. */ - model: string; - /** Literal API key — sealed into the `SecretStore`, never persisted as-is. */ - apiKey: string; -} - /** * Local model servers (F35). CRUD over the global registry of declared * `llama.cpp` servers an OpenCode profile can bind to via