feat: finalise ticket99 - implémentation agent model configuration v2
- 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
This commit is contained in:
@ -40,27 +40,26 @@ use crate::dto::{
|
|||||||
parse_ticket_id, save_model_server_input, AgentDriftListDto, AgentDto, AgentListDto,
|
parse_ticket_id, save_model_server_input, AgentDriftListDto, AgentDto, AgentListDto,
|
||||||
AppExitWorkGuardStateDto, AssignSkillRequestDto, AttachLiveAgentRequestDto,
|
AppExitWorkGuardStateDto, AssignSkillRequestDto, AttachLiveAgentRequestDto,
|
||||||
AttachLiveAgentResponseDto, BackgroundTaskDto, ChangeAgentProfileDto,
|
AttachLiveAgentResponseDto, BackgroundTaskDto, ChangeAgentProfileDto,
|
||||||
ChangeAgentProfileRequestDto, ClaudeProviderListDto, CloneOpenCodeProfileFromSeedRequestDto,
|
ChangeAgentProfileRequestDto, CloneOpenCodeProfileFromSeedRequestDto,
|
||||||
CodexProviderListDto, ConfigureProfilesRequestDto, ConversationDetailsDto,
|
ConfigureProfilesRequestDto, ConversationDetailsDto, CreateAgentFromTemplateRequestDto,
|
||||||
CreateAgentFromTemplateRequestDto, CreateAgentRequestDto, CreateLayoutRequestDto,
|
CreateAgentRequestDto, CreateLayoutRequestDto, CreateLayoutResultDto, CreateMemoryRequestDto,
|
||||||
CreateLayoutResultDto, CreateMemoryRequestDto, CreateProjectRequestDto, CreateSkillRequestDto,
|
CreateProjectRequestDto, CreateSkillRequestDto, CreateTemplateRequestDto,
|
||||||
CreateTemplateRequestDto, DeleteLayoutRequestDto, DeleteLayoutResultDto,
|
DeleteLayoutRequestDto, DeleteLayoutResultDto, DeliveredDelegationRequestDto,
|
||||||
DeliveredDelegationRequestDto, DetectProfilesRequestDto, DetectProfilesResponseDto,
|
DetectProfilesRequestDto, DetectProfilesResponseDto, EffectivePermissionsDto,
|
||||||
EffectivePermissionsDto, EmbedderEnginesDto, EmbedderProfileDto, EmbedderProfileListDto,
|
EmbedderEnginesDto, EmbedderProfileDto, EmbedderProfileListDto, ErrorDto, FirstRunStateDto,
|
||||||
ErrorDto, FirstRunStateDto, FrontAttachedRequestDto, GitBranchesDto, GitCheckoutRequestDto,
|
FrontAttachedRequestDto, GitBranchesDto, GitCheckoutRequestDto, GitCommitDto, GitCommitListDto,
|
||||||
GitCommitDto, GitCommitListDto, GitCommitRequestDto, GitStageRequestDto, GitStatusListDto,
|
GitCommitRequestDto, GitStageRequestDto, GitStatusListDto, GraphCommitListDto,
|
||||||
GraphCommitListDto, HealthRequestDto, HealthResponseDto, InspectConversationRequestDto,
|
HealthRequestDto, HealthResponseDto, InspectConversationRequestDto, InterruptAgentRequestDto,
|
||||||
InterruptAgentRequestDto, LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto,
|
LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto, LiveAgentListDto,
|
||||||
LiveAgentListDto, MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto,
|
MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto, ModelServerConfigDto,
|
||||||
ModelServerConfigDto, ModelServerConfigListDto, OpenCodeProviderListDto,
|
ModelServerConfigListDto, OpenCodeProviderListDto, OpenTerminalRequestDto,
|
||||||
OpenTerminalRequestDto, PreviewModelServerCommandDto, ProfileDto, ProfileListDto, ProjectDto,
|
PreviewModelServerCommandDto, ProfileDto, ProfileListDto, ProjectDto, ProjectListDto,
|
||||||
ProjectListDto, ProjectMcpToolPermissionsDto, ProjectPermissionsDto,
|
ProjectMcpToolPermissionsDto, ProjectPermissionsDto, ProjectSystemPermissionsDto,
|
||||||
ProjectSystemPermissionsDto, ProjectWorkStateDto, ReadAgentContextResponseDto,
|
ProjectWorkStateDto, ReadAgentContextResponseDto, ReadConversationPageRequestDto,
|
||||||
ReadConversationPageRequestDto, ReattachChatDto, ReattachResultDto, RecallMemoryRequestDto,
|
ReattachChatDto, ReattachResultDto, RecallMemoryRequestDto, RenameLayoutRequestDto, ReplyChunk,
|
||||||
RenameLayoutRequestDto, ReplyChunk, ResizeTerminalRequestDto,
|
ResizeTerminalRequestDto, ResolveAgentPermissionsRequestDto,
|
||||||
ResolveAgentPermissionsRequestDto, ResolveAgentSystemPermissionsRequestDto,
|
ResolveAgentSystemPermissionsRequestDto, ResolvedAgentSystemPermissionsDto,
|
||||||
ResolvedAgentSystemPermissionsDto, ResumableAgentListDto, SaveClaudeProviderProfileRequestDto,
|
ResumableAgentListDto, SaveEmbedderProfileRequestDto, SaveModelServerRequestDto,
|
||||||
SaveCodexProviderProfileRequestDto, SaveEmbedderProfileRequestDto, SaveModelServerRequestDto,
|
|
||||||
SaveOpenCodeProviderProfileRequestDto, SaveProfileRequestDto, SetActiveLayoutRequestDto,
|
SaveOpenCodeProviderProfileRequestDto, SaveProfileRequestDto, SetActiveLayoutRequestDto,
|
||||||
SetActiveLayoutResultDto, SkillDto, SkillListDto, StopLiveAgentRequestDto,
|
SetActiveLayoutResultDto, SkillDto, SkillListDto, StopLiveAgentRequestDto,
|
||||||
StopLiveAgentResponseDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto,
|
StopLiveAgentResponseDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto,
|
||||||
@ -1189,22 +1188,6 @@ pub async fn list_opencode_providers(
|
|||||||
Ok(state.list_opencode_providers.execute().into())
|
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<CodexProviderListDto, ErrorDto> {
|
|
||||||
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<ClaudeProviderListDto, ErrorDto> {
|
|
||||||
Ok(state.list_claude_providers.execute().into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `save_opencode_provider_profile` — create or replace an OpenCode profile
|
/// `save_opencode_provider_profile` — create or replace an OpenCode profile
|
||||||
/// backed by a cloud provider (ticket #92, lot B3). The literal API key is
|
/// backed by a cloud provider (ticket #92, lot B3). The literal API key is
|
||||||
/// sealed into the `SecretStore`, never persisted in `profiles.json`.
|
/// sealed into the `SecretStore`, never persisted in `profiles.json`.
|
||||||
@ -1225,36 +1208,6 @@ pub async fn save_opencode_provider_profile(
|
|||||||
.map_err(ErrorDto::from)
|
.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<ProfileDto, ErrorDto> {
|
|
||||||
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<ProfileDto, ErrorDto> {
|
|
||||||
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
|
/// `clone_opencode_profile_from_seed` — create a new OpenCode profile instance
|
||||||
/// from the canonical `opencode-llamacpp` seed/template.
|
/// from the canonical `opencode-llamacpp` seed/template.
|
||||||
///
|
///
|
||||||
|
|||||||
@ -255,10 +255,6 @@ pub fn run() {
|
|||||||
commands::save_profile,
|
commands::save_profile,
|
||||||
commands::save_opencode_provider_profile,
|
commands::save_opencode_provider_profile,
|
||||||
commands::list_opencode_providers,
|
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::clone_opencode_profile_from_seed,
|
||||||
commands::delete_profile,
|
commands::delete_profile,
|
||||||
commands::configure_profiles,
|
commands::configure_profiles,
|
||||||
|
|||||||
@ -20,10 +20,7 @@ use domain::ports::{
|
|||||||
ProfileStore, ProjectStore, PtyPort, RemotePath, SecretStore, SessionPlan, SkillStore,
|
ProfileStore, ProjectStore, PtyPort, RemotePath, SecretStore, SessionPlan, SkillStore,
|
||||||
SpawnSpec, StoreError, StructuredProviderLaunchPolicy, SystemPermissionStore,
|
SpawnSpec, StoreError, StructuredProviderLaunchPolicy, SystemPermissionStore,
|
||||||
};
|
};
|
||||||
use domain::profile::{
|
use domain::profile::{McpConfigStrategy, OpenCodeProviderConfig, StructuredAdapter};
|
||||||
ClaudeProviderConfig, CodexProviderConfig, McpConfigStrategy, OpenCodeProviderConfig,
|
|
||||||
StructuredAdapter,
|
|
||||||
};
|
|
||||||
use domain::sandbox::{compile_sandbox_plan, SandboxContext, SandboxPlan};
|
use domain::sandbox::{compile_sandbox_plan, SandboxContext, SandboxPlan};
|
||||||
use domain::{
|
use domain::{
|
||||||
bound_handoff_summary, Agent, AgentId, AgentManifest, AgentOrigin, AgentProfile,
|
bound_handoff_summary, Agent, AgentId, AgentManifest, AgentOrigin, AgentProfile,
|
||||||
@ -1134,35 +1131,7 @@ fn build_structured_launch_policy(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn projection_model(profile: &AgentProfile) -> Option<&str> {
|
fn projection_model(profile: &AgentProfile) -> Option<&str> {
|
||||||
profile
|
profile.model.as_deref()
|
||||||
.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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Launches an agent: resolve profile + context, prepare the invocation, apply
|
/// 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)
|
self.ensure_local_model_server_for_opencode(&agent, &mut profile)
|
||||||
.await?;
|
.await?;
|
||||||
self.apply_profile_provider_env(&profile, &run_dir, &mut spec)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// 5a. ── INJECTION DE LA CONF MCP (cadrage v3, Décision 3) ──
|
// 5a. ── INJECTION DE LA CONF MCP (cadrage v3, Décision 3) ──
|
||||||
// Strictement APRÈS le convention file (étape 5) et AVANT le spawn /
|
// Strictement APRÈS le convention file (étape 5) et AVANT le spawn /
|
||||||
@ -2246,9 +2213,6 @@ impl LaunchAgent {
|
|||||||
project_root: project_root.as_str(),
|
project_root: project_root.as_str(),
|
||||||
run_dir: run_dir.as_str(),
|
run_dir: run_dir.as_str(),
|
||||||
model: projection_model(profile),
|
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);
|
let projection = projector.project(permissions, network, &ctx);
|
||||||
|
|
||||||
@ -2483,6 +2447,7 @@ impl LaunchAgent {
|
|||||||
&declaration,
|
&declaration,
|
||||||
run_dir.as_str(),
|
run_dir.as_str(),
|
||||||
project_root.as_str(),
|
project_root.as_str(),
|
||||||
|
profile.model.as_deref(),
|
||||||
);
|
);
|
||||||
let _ = self.fs.write(&path, rendered.as_bytes()).await;
|
let _ = self.fs.write(&path, rendered.as_bytes()).await;
|
||||||
}
|
}
|
||||||
@ -2494,6 +2459,7 @@ impl LaunchAgent {
|
|||||||
&declaration,
|
&declaration,
|
||||||
run_dir.as_str(),
|
run_dir.as_str(),
|
||||||
project_root.as_str(),
|
project_root.as_str(),
|
||||||
|
profile.model.as_deref(),
|
||||||
);
|
);
|
||||||
let _ = self.fs.write(&path, rendered.as_bytes()).await;
|
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<String, AppError> {
|
|
||||||
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<String, AppError> {
|
|
||||||
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(
|
async fn ensure_local_model_server_for_opencode(
|
||||||
&self,
|
&self,
|
||||||
agent: &Agent,
|
agent: &Agent,
|
||||||
@ -3148,6 +3056,11 @@ fn toml_string(s: &str) -> String {
|
|||||||
format!("\"{}\"", json_escape(s))
|
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
|
/// 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
|
/// the `[mcp_servers.idea]` table and ensures the run-dir + project-root trust
|
||||||
/// entries. The permission part (`sandbox_mode` / `approval_policy` + the
|
/// entries. The permission part (`sandbox_mode` / `approval_policy` + the
|
||||||
@ -3159,8 +3072,12 @@ fn codex_config_toml(
|
|||||||
mcp_declaration: &str,
|
mcp_declaration: &str,
|
||||||
run_dir: &str,
|
run_dir: &str,
|
||||||
project_root: &str,
|
project_root: &str,
|
||||||
|
model: Option<&str>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let mut text = existing.unwrap_or_default().to_owned();
|
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 = replace_toml_table(&text, "mcp_servers.idea", mcp_declaration.trim_end());
|
||||||
text = ensure_codex_trust(&text, run_dir);
|
text = ensure_codex_trust(&text, run_dir);
|
||||||
text = ensure_codex_trust(&text, project_root);
|
text = ensure_codex_trust(&text, project_root);
|
||||||
@ -4585,6 +4502,7 @@ command = "other"
|
|||||||
"[mcp_servers.idea]\ncommand = \"new\"",
|
"[mcp_servers.idea]\ncommand = \"new\"",
|
||||||
"/home/me/proj/.ideai/run/a",
|
"/home/me/proj/.ideai/run/a",
|
||||||
"/home/me/proj",
|
"/home/me/proj",
|
||||||
|
Some("gpt-5-codex"),
|
||||||
);
|
);
|
||||||
|
|
||||||
// MCP table + trust entries are the only things this function touches.
|
// 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("approval_policy = \"nested\""));
|
||||||
assert!(rendered.contains("[mcp_servers.idea]\ncommand = \"new\""));
|
assert!(rendered.contains("[mcp_servers.idea]\ncommand = \"new\""));
|
||||||
assert!(rendered.contains("[mcp_servers.other]\ncommand = \"other\""));
|
assert!(rendered.contains("[mcp_servers.other]\ncommand = \"other\""));
|
||||||
|
assert!(rendered.contains("model = \"gpt-5-codex\""));
|
||||||
assert!(!rendered.contains("command = \"old\""));
|
assert!(!rendered.contains("command = \"old\""));
|
||||||
assert_eq!(rendered.matches("[mcp_servers.idea]").count(), 1);
|
assert_eq!(rendered.matches("[mcp_servers.idea]").count(), 1);
|
||||||
assert_eq!(rendered.matches("[projects.\"/home/me/proj\"]").count(), 1);
|
assert_eq!(rendered.matches("[projects.\"/home/me/proj\"]").count(), 1);
|
||||||
@ -4607,8 +4526,10 @@ command = "other"
|
|||||||
"[mcp_servers.idea]\ncommand = \"idea-mcp\"",
|
"[mcp_servers.idea]\ncommand = \"idea-mcp\"",
|
||||||
"/home/me/proj/.ideai/run/a",
|
"/home/me/proj/.ideai/run/a",
|
||||||
"/home/me/proj",
|
"/home/me/proj",
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert!(!rendered.contains("model ="));
|
||||||
assert!(!rendered.contains("approval_policy ="));
|
assert!(!rendered.contains("approval_policy ="));
|
||||||
assert!(!rendered.contains("sandbox_mode ="));
|
assert!(!rendered.contains("sandbox_mode ="));
|
||||||
assert!(rendered.contains("[projects.\"/home/me/proj\"]"));
|
assert!(rendered.contains("[projects.\"/home/me/proj\"]"));
|
||||||
|
|||||||
@ -40,10 +40,8 @@ pub use lifecycle::{
|
|||||||
AGENT_MEMORY_RECALL_BUDGET, DEFAULT_OPENCODE_MCP_TIMEOUT_MS, LIVE_STATE_INJECT_MAX,
|
AGENT_MEMORY_RECALL_BUDGET, DEFAULT_OPENCODE_MCP_TIMEOUT_MS, LIVE_STATE_INJECT_MAX,
|
||||||
};
|
};
|
||||||
pub use provider_catalogue::{
|
pub use provider_catalogue::{
|
||||||
claude_provider_catalogue, codex_provider_catalogue, opencode_models_cache_path,
|
opencode_models_cache_path, opencode_provider_catalogue, ListOpenCodeProviders,
|
||||||
opencode_provider_catalogue, ClaudeProviderCatalogEntry, CodexProviderCatalogEntry,
|
ListOpenCodeProvidersOutput, OpenCodeProviderCatalogEntry,
|
||||||
ListClaudeProviders, ListClaudeProvidersOutput, ListCodexProviders, ListCodexProvidersOutput,
|
|
||||||
ListOpenCodeProviders, ListOpenCodeProvidersOutput, OpenCodeProviderCatalogEntry,
|
|
||||||
};
|
};
|
||||||
pub use resume::{
|
pub use resume::{
|
||||||
ListResumableAgents, ListResumableAgentsInput, ListResumableAgentsOutput, ResumableAgent,
|
ListResumableAgents, ListResumableAgentsInput, ListResumableAgentsOutput, ResumableAgent,
|
||||||
@ -54,8 +52,6 @@ pub use usecases::{
|
|||||||
ConfigureProfilesOutput, DeleteProfile, DeleteProfileInput, DetectProfiles,
|
ConfigureProfilesOutput, DeleteProfile, DeleteProfileInput, DetectProfiles,
|
||||||
DetectProfilesInput, DetectProfilesOutput, FirstRunState, FirstRunStateOutput, ListProfiles,
|
DetectProfilesInput, DetectProfilesOutput, FirstRunState, FirstRunStateOutput, ListProfiles,
|
||||||
ListProfilesOutput, ProfileAvailability, ReferenceProfiles, ReferenceProfilesOutput,
|
ListProfilesOutput, ProfileAvailability, ReferenceProfiles, ReferenceProfilesOutput,
|
||||||
SaveClaudeProviderProfile, SaveClaudeProviderProfileInput, SaveClaudeProviderProfileOutput,
|
|
||||||
SaveCodexProviderProfile, SaveCodexProviderProfileInput, SaveCodexProviderProfileOutput,
|
|
||||||
SaveOpenCodeProviderProfile, SaveOpenCodeProviderProfileInput,
|
SaveOpenCodeProviderProfile, SaveOpenCodeProviderProfileInput,
|
||||||
SaveOpenCodeProviderProfileOutput, SaveProfile, SaveProfileInput, SaveProfileOutput,
|
SaveOpenCodeProviderProfileOutput, SaveProfile, SaveProfileInput, SaveProfileOutput,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -41,60 +41,6 @@ pub struct OpenCodeProviderCatalogEntry {
|
|||||||
pub(crate) model_labels: BTreeMap<String, String>,
|
pub(crate) model_labels: BTreeMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<String>,
|
|
||||||
/// 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<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the static Codex provider catalogue.
|
|
||||||
#[must_use]
|
|
||||||
pub fn codex_provider_catalogue() -> Vec<CodexProviderCatalogEntry> {
|
|
||||||
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<ClaudeProviderCatalogEntry> {
|
|
||||||
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 original lot-B3 catalogue: three well-known providers, used whenever
|
||||||
/// the real OpenCode model cache can't be read or parsed.
|
/// the real OpenCode model cache can't be read or parsed.
|
||||||
fn static_fallback_catalogue() -> Vec<OpenCodeProviderCatalogEntry> {
|
fn static_fallback_catalogue() -> Vec<OpenCodeProviderCatalogEntry> {
|
||||||
@ -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<CodexProviderCatalogEntry>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<ClaudeProviderCatalogEntry>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@ -16,8 +16,7 @@ use std::sync::Arc;
|
|||||||
use domain::ids::ProfileId;
|
use domain::ids::ProfileId;
|
||||||
use domain::ports::{AgentRuntime, IdGenerator, ProfileStore, SecretRef, SecretStore};
|
use domain::ports::{AgentRuntime, IdGenerator, ProfileStore, SecretRef, SecretStore};
|
||||||
use domain::profile::{
|
use domain::profile::{
|
||||||
AgentProfile, ClaudeProviderConfig, CodexCustomProviderConfig, CodexProviderConfig,
|
AgentProfile, CustomProviderConfig, OpenCodeConfig, OpenCodeProviderConfig, StructuredAdapter,
|
||||||
CustomProviderConfig, OpenCodeConfig, OpenCodeProviderConfig, StructuredAdapter,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
@ -325,149 +324,6 @@ pub struct SaveOpenCodeProviderProfileOutput {
|
|||||||
pub profile: AgentProfile,
|
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<CodexCustomProviderConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<dyn ProfileStore>,
|
|
||||||
secret_store: Arc<dyn SecretStore>,
|
|
||||||
ids: Arc<dyn IdGenerator>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SaveCodexProviderProfile {
|
|
||||||
/// Builds the use case from the profile store, secret store and id generator
|
|
||||||
/// ports.
|
|
||||||
#[must_use]
|
|
||||||
pub fn new(
|
|
||||||
profile_store: Arc<dyn ProfileStore>,
|
|
||||||
secret_store: Arc<dyn SecretStore>,
|
|
||||||
ids: Arc<dyn IdGenerator>,
|
|
||||||
) -> 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<SaveCodexProviderProfileOutput, AppError> {
|
|
||||||
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<dyn ProfileStore>,
|
|
||||||
secret_store: Arc<dyn SecretStore>,
|
|
||||||
ids: Arc<dyn IdGenerator>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SaveClaudeProviderProfile {
|
|
||||||
/// Builds the use case from the profile store, secret store and id generator
|
|
||||||
/// ports.
|
|
||||||
#[must_use]
|
|
||||||
pub fn new(
|
|
||||||
profile_store: Arc<dyn ProfileStore>,
|
|
||||||
secret_store: Arc<dyn SecretStore>,
|
|
||||||
ids: Arc<dyn IdGenerator>,
|
|
||||||
) -> 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<SaveClaudeProviderProfileOutput, AppError> {
|
|
||||||
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
|
/// 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
|
/// 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
|
/// [`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 {
|
if let Some(config) = &profile.opencode_provider {
|
||||||
self.secret_store.delete(&config.api_key_ref).await?;
|
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?;
|
self.store.delete(input.id).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -44,26 +44,23 @@ pub use agent::{
|
|||||||
drain_with_readiness_and_announcements, drain_with_readiness_outcome, reference_profile_id,
|
drain_with_readiness_and_announcements, drain_with_readiness_outcome, reference_profile_id,
|
||||||
reference_profiles, selectable_reference_profiles, send_blocking, AgentResumer,
|
reference_profiles, selectable_reference_profiles, send_blocking, AgentResumer,
|
||||||
AnnouncementPublisher, ChangeAgentProfile, ChangeAgentProfileInput, ChangeAgentProfileOutput,
|
AnnouncementPublisher, ChangeAgentProfile, ChangeAgentProfileInput, ChangeAgentProfileOutput,
|
||||||
ClaudeProviderCatalogEntry, CloneOpenCodeProfileFromSeed, CloneOpenCodeProfileFromSeedInput,
|
CloneOpenCodeProfileFromSeed, CloneOpenCodeProfileFromSeedInput,
|
||||||
CloneOpenCodeProfileFromSeedOutput, CodexProviderCatalogEntry, ConfigureProfiles,
|
CloneOpenCodeProfileFromSeedOutput, ConfigureProfiles, ConfigureProfilesInput,
|
||||||
ConfigureProfilesInput, ConfigureProfilesOutput, CreateAgentFromScratch, CreateAgentInput,
|
ConfigureProfilesOutput, CreateAgentFromScratch, CreateAgentInput, CreateAgentOutput,
|
||||||
CreateAgentOutput, DeleteAgent, DeleteAgentInput, DeleteProfile, DeleteProfileInput,
|
DeleteAgent, DeleteAgentInput, DeleteProfile, DeleteProfileInput, DetectProfiles,
|
||||||
DetectProfiles, DetectProfilesInput, DetectProfilesOutput, FirstRunState, FirstRunStateOutput,
|
DetectProfilesInput, DetectProfilesOutput, FirstRunState, FirstRunStateOutput, HandoffProvider,
|
||||||
HandoffProvider, InjectedLiveRow, InspectConversation, InspectConversationInput,
|
InjectedLiveRow, InspectConversation, InspectConversationInput, InspectConversationOutput,
|
||||||
InspectConversationOutput, LaunchAgent, LaunchAgentInput, LaunchAgentOutput, ListAgents,
|
LaunchAgent, LaunchAgentInput, LaunchAgentOutput, ListAgents, ListAgentsInput,
|
||||||
ListAgentsInput, ListAgentsOutput, ListClaudeProviders, ListClaudeProvidersOutput,
|
ListAgentsOutput, ListOpenCodeProviders, ListOpenCodeProvidersOutput, ListProfiles,
|
||||||
ListCodexProviders, ListCodexProvidersOutput, ListOpenCodeProviders,
|
ListProfilesOutput, ListResumableAgents, ListResumableAgentsInput, ListResumableAgentsOutput,
|
||||||
ListOpenCodeProvidersOutput, ListProfiles, ListProfilesOutput, ListResumableAgents,
|
LiveStateLeanProvider, McpRuntime, OpenCodeProviderCatalogEntry, PermissionProjectorRegistry,
|
||||||
ListResumableAgentsInput, ListResumableAgentsOutput, LiveStateLeanProvider, McpRuntime,
|
ProfileAvailability, ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput,
|
||||||
OpenCodeProviderCatalogEntry, PermissionProjectorRegistry, ProfileAvailability,
|
ReadAgentContextOutput, ReferenceProfiles, ReferenceProfilesOutput, ResumableAgent,
|
||||||
ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput,
|
SaveOpenCodeProviderProfile, SaveOpenCodeProviderProfileInput,
|
||||||
ReferenceProfiles, ReferenceProfilesOutput, ResumableAgent, SaveClaudeProviderProfile,
|
SaveOpenCodeProviderProfileOutput, SaveProfile, SaveProfileInput, SaveProfileOutput,
|
||||||
SaveClaudeProviderProfileInput, SaveClaudeProviderProfileOutput, SaveCodexProviderProfile,
|
SessionLimitService, StructuredRoutingMode, StructuredSessionDescriptor, TurnOutcome,
|
||||||
SaveCodexProviderProfileInput, SaveCodexProviderProfileOutput, SaveOpenCodeProviderProfile,
|
UpdateAgentContext, UpdateAgentContextInput, AGENT_MEMORY_RECALL_BUDGET, CODEX_SUBMIT_DELAY_MS,
|
||||||
SaveOpenCodeProviderProfileInput, SaveOpenCodeProviderProfileOutput, SaveProfile,
|
LIVE_STATE_INJECT_MAX, RESUME_PROMPT,
|
||||||
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::{
|
pub use background::{
|
||||||
BackgroundCommandArchive, CancelBackgroundTask, CancelBackgroundTaskOutput,
|
BackgroundCommandArchive, CancelBackgroundTask, CancelBackgroundTaskOutput,
|
||||||
|
|||||||
@ -27,9 +27,8 @@ use application::{
|
|||||||
reference_profile_id, reference_profiles, CloneOpenCodeProfileFromSeed,
|
reference_profile_id, reference_profiles, CloneOpenCodeProfileFromSeed,
|
||||||
CloneOpenCodeProfileFromSeedInput, ConfigureProfiles, ConfigureProfilesInput, DeleteProfile,
|
CloneOpenCodeProfileFromSeedInput, ConfigureProfiles, ConfigureProfilesInput, DeleteProfile,
|
||||||
DeleteProfileInput, DetectProfiles, DetectProfilesInput, FirstRunState, ListProfiles,
|
DeleteProfileInput, DetectProfiles, DetectProfilesInput, FirstRunState, ListProfiles,
|
||||||
ReferenceProfiles, SaveClaudeProviderProfile, SaveClaudeProviderProfileInput,
|
ReferenceProfiles, SaveOpenCodeProviderProfile, SaveOpenCodeProviderProfileInput, SaveProfile,
|
||||||
SaveCodexProviderProfile, SaveCodexProviderProfileInput, SaveOpenCodeProviderProfile,
|
SaveProfileInput, CODEX_SUBMIT_DELAY_MS,
|
||||||
SaveOpenCodeProviderProfileInput, SaveProfile, SaveProfileInput, CODEX_SUBMIT_DELAY_MS,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -602,70 +601,25 @@ async fn delete_profile_with_opencode_provider_purges_its_secret() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 store = FakeProfileStore::default();
|
||||||
let secrets = FakeSecretStore::default();
|
let save = SaveProfile::new(Arc::new(store.clone()));
|
||||||
let save = SaveCodexProviderProfile::new(
|
let profile = profile(991, "Codex GPT-5", "codex").with_model("gpt-5-codex");
|
||||||
Arc::new(store.clone()),
|
|
||||||
Arc::new(secrets.clone()),
|
|
||||||
Arc::new(SeqIds::new(vec![uuid::Uuid::from_u128(9901)])),
|
|
||||||
);
|
|
||||||
|
|
||||||
let out = save
|
let out = save
|
||||||
.execute(SaveCodexProviderProfileInput {
|
.execute(SaveProfileInput {
|
||||||
profile: profile(991, "Codex GPT-5", "codex"),
|
profile: profile.clone(),
|
||||||
provider_id: "openai".to_owned(),
|
|
||||||
model: "gpt-5".to_owned(),
|
|
||||||
api_key: "sk-openai-literal".to_owned(),
|
|
||||||
custom: None,
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let provider = out.profile.codex_provider.as_ref().unwrap();
|
assert_eq!(out.profile.model.as_deref(), Some("gpt-5-codex"));
|
||||||
assert_eq!(provider.provider_id, "openai");
|
let json = serde_json::to_string(&out.profile).unwrap();
|
||||||
assert_eq!(provider.model, "gpt-5");
|
assert!(!json.contains("codexProvider"), "got: {json}");
|
||||||
assert_ne!(provider.api_key_ref.as_str(), "sk-openai-literal");
|
assert!(!json.contains("claudeProvider"), "got: {json}");
|
||||||
assert!(!serde_json::to_string(&out.profile)
|
assert!(!json.contains("apiKey"), "got: {json}");
|
||||||
.unwrap()
|
assert!(!json.contains("providerId"), "got: {json}");
|
||||||
.contains("sk-openai-literal"));
|
assert_eq!(store.list().await.unwrap(), vec![profile]);
|
||||||
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())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@ -999,13 +999,10 @@ use application::{
|
|||||||
CloneOpenCodeProfileFromSeedInput, CloneOpenCodeProfileFromSeedOutput, ConfigureProfilesInput,
|
CloneOpenCodeProfileFromSeedInput, CloneOpenCodeProfileFromSeedOutput, ConfigureProfilesInput,
|
||||||
ConfigureProfilesOutput, DeleteProfileInput, DetectProfilesInput, DetectProfilesOutput,
|
ConfigureProfilesOutput, DeleteProfileInput, DetectProfilesInput, DetectProfilesOutput,
|
||||||
FirstRunStateOutput, ListProfilesOutput, ProfileAvailability, ReferenceProfilesOutput,
|
FirstRunStateOutput, ListProfilesOutput, ProfileAvailability, ReferenceProfilesOutput,
|
||||||
SaveClaudeProviderProfileInput, SaveClaudeProviderProfileOutput, SaveCodexProviderProfileInput,
|
SaveOpenCodeProviderProfileInput, SaveOpenCodeProviderProfileOutput, SaveProfileInput,
|
||||||
SaveCodexProviderProfileOutput, SaveOpenCodeProviderProfileInput,
|
SaveProfileOutput,
|
||||||
SaveOpenCodeProviderProfileOutput, SaveProfileInput, SaveProfileOutput,
|
|
||||||
};
|
|
||||||
use domain::profile::{
|
|
||||||
AgentProfile, CodexCustomProviderConfig, CustomProviderConfig, OpenCodeConfig,
|
|
||||||
};
|
};
|
||||||
|
use domain::profile::{AgentProfile, CustomProviderConfig, OpenCodeConfig};
|
||||||
use domain::ProfileId;
|
use domain::ProfileId;
|
||||||
|
|
||||||
/// A profile crossing the wire. [`AgentProfile`] already serialises camelCase
|
/// A profile crossing the wire. [`AgentProfile`] already serialises camelCase
|
||||||
@ -1072,68 +1069,6 @@ impl From<application::OpenCodeProviderCatalogEntry> 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<String>,
|
|
||||||
pub custom_supported: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<application::CodexProviderCatalogEntry> 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<CodexProviderDto>);
|
|
||||||
|
|
||||||
impl From<application::ListCodexProvidersOutput> 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<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<application::ClaudeProviderCatalogEntry> 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<ClaudeProviderDto>);
|
|
||||||
|
|
||||||
impl From<application::ListClaudeProvidersOutput> 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
|
/// A list of OpenCode cloud-provider catalogue entries (camelCase array on the
|
||||||
/// wire).
|
/// wire).
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@ -1246,63 +1181,6 @@ impl From<SaveOpenCodeProviderProfileOutput> 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<CodexCustomProviderConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SaveCodexProviderProfileRequestDto> 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<SaveCodexProviderProfileOutput> 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<SaveClaudeProviderProfileRequestDto> 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<SaveClaudeProviderProfileOutput> for ProfileDto {
|
|
||||||
fn from(out: SaveClaudeProviderProfileOutput) -> Self {
|
|
||||||
Self(out.profile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Request DTO for `clone_opencode_profile_from_seed`.
|
/// Request DTO for `clone_opencode_profile_from_seed`.
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|||||||
@ -26,13 +26,13 @@ use application::{
|
|||||||
GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage, GitStatus, GitUnstage,
|
GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage, GitStatus, GitUnstage,
|
||||||
HarvestMemoryFromTurn, HealthUseCase, InspectConversation, InstallPluginFromArchive,
|
HarvestMemoryFromTurn, HealthUseCase, InspectConversation, InstallPluginFromArchive,
|
||||||
InstallPluginFromDirectory, JsonPluginManifestValidator, LaunchAgent, LaunchAgentInput,
|
InstallPluginFromDirectory, JsonPluginManifestValidator, LaunchAgent, LaunchAgentInput,
|
||||||
LinkIssues, ListAgents, ListAgentsInput, ListClaudeProviders, ListCodexProviders, ListDevices,
|
LinkIssues, ListAgents, ListAgentsInput, ListDevices, ListEmbedderProfiles, ListIssues,
|
||||||
ListEmbedderProfiles, ListIssues, ListLayouts, ListMemories, ListModelServers,
|
ListLayouts, ListMemories, ListModelServers, ListOpenCodeProviders,
|
||||||
ListOpenCodeProviders, ListPluginRuntimeContributions, ListPlugins, ListProfiles, ListProjects,
|
ListPluginRuntimeContributions, ListPlugins, ListProfiles, ListProjects, ListResumableAgents,
|
||||||
ListResumableAgents, ListSkills, ListSprints, ListTemplates, LiveAgentRegistry, LiveSessions,
|
ListSkills, ListSprints, ListTemplates, LiveAgentRegistry, LiveSessions, LiveStateLeanProvider,
|
||||||
LiveStateLeanProvider, LiveStateProvider, LiveStateReadProvider, LoadLayout, McpRuntime,
|
LiveStateProvider, LiveStateReadProvider, LoadLayout, McpRuntime, McpToolPermissionCatalogue,
|
||||||
McpToolPermissionCatalogue, MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject,
|
MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject, OpenTerminal,
|
||||||
OpenTerminal, OpenTicketAssistant, OrchestratorService, PairAttemptLimiter, PairDevice,
|
OpenTicketAssistant, OrchestratorService, PairAttemptLimiter, PairDevice,
|
||||||
PermissionProjectorRegistry, ProposeContext, ReadAgentContext, ReadContext,
|
PermissionProjectorRegistry, ProposeContext, ReadAgentContext, ReadContext,
|
||||||
ReadConversationPage, ReadIssue, ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory,
|
ReadConversationPage, ReadIssue, ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory,
|
||||||
ReadMemoryIndex, ReadProjectContext, ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts,
|
ReadMemoryIndex, ReadProjectContext, ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts,
|
||||||
@ -40,17 +40,17 @@ use application::{
|
|||||||
RecordTurnProvider, ReferenceProfiles, RenameDevice, RenameLayout, RenameSprint,
|
RecordTurnProvider, ReferenceProfiles, RenameDevice, RenameLayout, RenameSprint,
|
||||||
ReorderSprints, ResizeTerminal, ResolveAgentPermissions, ResolveAgentSystemPermissions,
|
ReorderSprints, ResizeTerminal, ResolveAgentPermissions, ResolveAgentSystemPermissions,
|
||||||
ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask, ReviewPluginPackage,
|
ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask, ReviewPluginPackage,
|
||||||
RevokeAllDevices, RevokeDevice, RotateConversationLog, SaveClaudeProviderProfile,
|
RevokeAllDevices, RevokeDevice, RotateConversationLog, SaveEmbedderProfile, SaveModelServer,
|
||||||
SaveCodexProviderProfile, SaveEmbedderProfile, SaveModelServer, SaveOpenCodeProviderProfile,
|
SaveOpenCodeProviderProfile, SaveProfile, SessionLimitService, SetActiveLayout,
|
||||||
SaveProfile, SessionLimitService, SetActiveLayout, SetPluginEnabled, SnapshotOpenWindows,
|
SetPluginEnabled, SnapshotOpenWindows, SnapshotRunningAgents, SpawnBackgroundCommand,
|
||||||
SnapshotRunningAgents, SpawnBackgroundCommand, StopLiveAgent, StructuredRoutingMode,
|
StopLiveAgent, StructuredRoutingMode, StructuredSessions, SuggestedThisSession,
|
||||||
StructuredSessions, SuggestedThisSession, SyncAgentWithTemplate, TerminalSessions, TouchDevice,
|
SyncAgentWithTemplate, TerminalSessions, TouchDevice, UnassignSkillFromAgent,
|
||||||
UnassignSkillFromAgent, UnassignTicketFromSprint, UninstallPlugin, UnlinkIssues,
|
UnassignTicketFromSprint, UninstallPlugin, UnlinkIssues, UpdateAgentContext,
|
||||||
UpdateAgentContext, UpdateAgentMcpToolPermissions, UpdateAgentPermissions,
|
UpdateAgentMcpToolPermissions, UpdateAgentPermissions, UpdateAgentSystemPermissions,
|
||||||
UpdateAgentSystemPermissions, UpdateIssue, UpdateIssueCarnet, UpdateLiveState, UpdateMemory,
|
UpdateIssue, UpdateIssueCarnet, UpdateLiveState, UpdateMemory, UpdateProjectContext,
|
||||||
UpdateProjectContext, UpdateProjectMcpToolPermissions, UpdateProjectPermissions,
|
UpdateProjectMcpToolPermissions, UpdateProjectPermissions, UpdateProjectSystemPermissions,
|
||||||
UpdateProjectSystemPermissions, UpdateSkill, UpdateTemplate, WakeSessionProvider, WriteMemory,
|
UpdateSkill, UpdateTemplate, WakeSessionProvider, WriteMemory, WriteToTerminal,
|
||||||
WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
|
AGENT_MEMORY_RECALL_BUDGET,
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use domain::ports::{
|
use domain::ports::{
|
||||||
@ -938,18 +938,8 @@ pub struct BackendCore {
|
|||||||
/// Save (upsert) an OpenCode profile backed by a cloud provider, sealing its
|
/// Save (upsert) an OpenCode profile backed by a cloud provider, sealing its
|
||||||
/// literal API key into the [`SecretStore`] (ticket #92, lot B3).
|
/// literal API key into the [`SecretStore`] (ticket #92, lot B3).
|
||||||
pub save_opencode_provider_profile: Arc<SaveOpenCodeProviderProfile>,
|
pub save_opencode_provider_profile: Arc<SaveOpenCodeProviderProfile>,
|
||||||
/// 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<SaveCodexProviderProfile>,
|
|
||||||
/// 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<SaveClaudeProviderProfile>,
|
|
||||||
/// Static catalogue of OpenCode cloud providers (ticket #92, lot B3).
|
/// Static catalogue of OpenCode cloud providers (ticket #92, lot B3).
|
||||||
pub list_opencode_providers: Arc<ListOpenCodeProviders>,
|
pub list_opencode_providers: Arc<ListOpenCodeProviders>,
|
||||||
/// Static catalogue of Codex providers (ticket #99).
|
|
||||||
pub list_codex_providers: Arc<ListCodexProviders>,
|
|
||||||
/// Static catalogue of Claude providers (ticket #99).
|
|
||||||
pub list_claude_providers: Arc<ListClaudeProviders>,
|
|
||||||
/// Create a new OpenCode profile instance from the canonical seed.
|
/// Create a new OpenCode profile instance from the canonical seed.
|
||||||
pub clone_opencode_profile_from_seed: Arc<CloneOpenCodeProfileFromSeed>,
|
pub clone_opencode_profile_from_seed: Arc<CloneOpenCodeProfileFromSeed>,
|
||||||
/// Delete a profile.
|
/// Delete a profile.
|
||||||
@ -1477,19 +1467,7 @@ impl BackendCore {
|
|||||||
Arc::clone(&secret_store_port),
|
Arc::clone(&secret_store_port),
|
||||||
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||||
));
|
));
|
||||||
let save_codex_provider_profile = Arc::new(SaveCodexProviderProfile::new(
|
|
||||||
Arc::clone(&profile_store_port),
|
|
||||||
Arc::clone(&secret_store_port),
|
|
||||||
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
|
||||||
));
|
|
||||||
let save_claude_provider_profile = Arc::new(SaveClaudeProviderProfile::new(
|
|
||||||
Arc::clone(&profile_store_port),
|
|
||||||
Arc::clone(&secret_store_port),
|
|
||||||
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
|
||||||
));
|
|
||||||
let list_opencode_providers = Arc::new(ListOpenCodeProviders::new());
|
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(
|
let clone_opencode_profile_from_seed = Arc::new(CloneOpenCodeProfileFromSeed::new(
|
||||||
Arc::clone(&profile_store_port),
|
Arc::clone(&profile_store_port),
|
||||||
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||||
@ -2682,11 +2660,7 @@ impl BackendCore {
|
|||||||
list_profiles,
|
list_profiles,
|
||||||
save_profile,
|
save_profile,
|
||||||
save_opencode_provider_profile,
|
save_opencode_provider_profile,
|
||||||
save_codex_provider_profile,
|
|
||||||
save_claude_provider_profile,
|
|
||||||
list_opencode_providers,
|
list_opencode_providers,
|
||||||
list_codex_providers,
|
|
||||||
list_claude_providers,
|
|
||||||
clone_opencode_profile_from_seed,
|
clone_opencode_profile_from_seed,
|
||||||
delete_profile,
|
delete_profile,
|
||||||
configure_profiles,
|
configure_profiles,
|
||||||
|
|||||||
@ -853,12 +853,6 @@ pub struct ProjectionContext<'a> {
|
|||||||
/// Optional model selected by the agent profile. Orthogonal to permissions:
|
/// Optional model selected by the agent profile. Orthogonal to permissions:
|
||||||
/// projectors may still materialise it even when `eff == None`.
|
/// projectors may still materialise it even when `eff == None`.
|
||||||
pub model: Option<&'a str>,
|
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**.
|
/// One file a projector wants materialised at launch, tagged by **ownership**.
|
||||||
|
|||||||
@ -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<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<String>,
|
|
||||||
display_name: Option<String>,
|
|
||||||
) -> Result<Self, DomainError> {
|
|
||||||
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<CodexCustomProviderConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<String>,
|
|
||||||
model: impl Into<String>,
|
|
||||||
api_key_ref: crate::ports::SecretRef,
|
|
||||||
) -> Result<Self, DomainError> {
|
|
||||||
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<String>,
|
|
||||||
model: impl Into<String>,
|
|
||||||
api_key_ref: crate::ports::SecretRef,
|
|
||||||
) -> Result<Self, DomainError> {
|
|
||||||
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
|
/// Configuration additive d'un provider OpenCode **personnalisé** (endpoint
|
||||||
/// OpenAI-compatible arbitraire, hors catalogue OpenCode), portée par
|
/// OpenAI-compatible arbitraire, hors catalogue OpenCode), portée par
|
||||||
/// [`OpenCodeProviderConfig::custom`].
|
/// [`OpenCodeProviderConfig::custom`].
|
||||||
@ -1015,16 +896,11 @@ pub struct AgentProfile {
|
|||||||
/// avant.
|
/// avant.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub opencode_provider: Option<OpenCodeProviderConfig>,
|
pub opencode_provider: Option<OpenCodeProviderConfig>,
|
||||||
/// Configuration Codex provider/modèle (ticket #99). `None` pour les profils
|
/// Modèle explicitement configuré pour les CLIs qui supportent un réglage
|
||||||
/// non-Codex et les profils Codex historiques qui gardent le défaut natif de
|
/// déclaratif direct (Codex/Claude). `None` conserve le défaut natif de la
|
||||||
/// la CLI.
|
/// CLI. OpenCode garde ses champs dédiés.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub codex_provider: Option<CodexProviderConfig>,
|
pub model: Option<String>,
|
||||||
/// 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<ClaudeProviderConfig>,
|
|
||||||
/// Capacité **MCP** (ARCHITECTURE §14.3, orchestration v3, Décision 1).
|
/// Capacité **MCP** (ARCHITECTURE §14.3, orchestration v3, Décision 1).
|
||||||
/// `None` ⇒ repli fichier `.ideai/requests` + prose (comportement actuel).
|
/// `None` ⇒ repli fichier `.ideai/requests` + prose (comportement actuel).
|
||||||
/// `Some(_)` ⇒ IdeA matérialise la config MCP de cette CLI au lancement et
|
/// `Some(_)` ⇒ IdeA matérialise la config MCP de cette CLI au lancement et
|
||||||
@ -1231,8 +1107,7 @@ impl AgentProfile {
|
|||||||
chat_http: None,
|
chat_http: None,
|
||||||
opencode: None,
|
opencode: None,
|
||||||
opencode_provider: None,
|
opencode_provider: None,
|
||||||
codex_provider: None,
|
model: None,
|
||||||
claude_provider: None,
|
|
||||||
mcp: None,
|
mcp: None,
|
||||||
liveness: None,
|
liveness: None,
|
||||||
rate_limit_pattern: None,
|
rate_limit_pattern: None,
|
||||||
@ -1283,17 +1158,10 @@ impl AgentProfile {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder : fixe la configuration Codex provider/modèle (ticket #99).
|
/// Builder : fixe le modèle CLI direct (Codex/Claude, ticket #99).
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_codex_provider(mut self, config: CodexProviderConfig) -> Self {
|
pub fn with_model(mut self, model: impl Into<String>) -> Self {
|
||||||
self.codex_provider = Some(config);
|
self.model = Some(model.into());
|
||||||
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);
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1608,50 +1476,20 @@ mod mcp_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn codex_and_claude_provider_configs_round_trip_camelcase() {
|
fn profile_model_round_trips_without_codex_or_claude_provider_config() {
|
||||||
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();
|
|
||||||
let profile = profile_without_mcp()
|
let profile = profile_without_mcp()
|
||||||
.with_structured_adapter(StructuredAdapter::Codex)
|
.with_structured_adapter(StructuredAdapter::Codex)
|
||||||
.with_codex_provider(codex.clone())
|
.with_model("gpt-5-codex");
|
||||||
.with_claude_provider(claude.clone());
|
|
||||||
|
|
||||||
let json = serde_json::to_string(&profile).expect("serialise");
|
let json = serde_json::to_string(&profile).expect("serialise");
|
||||||
assert!(json.contains("\"codexProvider\""), "got: {json}");
|
assert!(json.contains("\"model\":\"gpt-5-codex\""), "got: {json}");
|
||||||
assert!(json.contains("\"claudeProvider\""), "got: {json}");
|
assert!(!json.contains("codexProvider"), "got: {json}");
|
||||||
assert!(json.contains("\"providerId\":\"openai\""), "got: {json}");
|
assert!(!json.contains("claudeProvider"), "got: {json}");
|
||||||
assert!(json.contains("\"baseUrl\""), "got: {json}");
|
assert!(!json.contains("apiKeyRef"), "got: {json}");
|
||||||
|
assert!(!json.contains("providerId"), "got: {json}");
|
||||||
|
|
||||||
let back: AgentProfile = serde_json::from_str(&json).expect("deserialise");
|
let back: AgentProfile = serde_json::from_str(&json).expect("deserialise");
|
||||||
assert_eq!(back.codex_provider, Some(codex));
|
assert_eq!(back.model.as_deref(), Some("gpt-5-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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -235,6 +235,7 @@ impl TicketAssistantEnvironmentPreparer {
|
|||||||
&declaration,
|
&declaration,
|
||||||
cwd.as_str(),
|
cwd.as_str(),
|
||||||
project.root.as_str(),
|
project.root.as_str(),
|
||||||
|
profile.model.as_deref(),
|
||||||
);
|
);
|
||||||
self.write_file(&path, rendered.as_bytes()).await?;
|
self.write_file(&path, rendered.as_bytes()).await?;
|
||||||
env.push((home_env.clone(), parent_dir(cwd, target)));
|
env.push((home_env.clone(), parent_dir(cwd, target)));
|
||||||
@ -511,12 +512,13 @@ fn codex_config_toml(
|
|||||||
mcp_declaration: &str,
|
mcp_declaration: &str,
|
||||||
run_dir: &str,
|
run_dir: &str,
|
||||||
project_root: &str,
|
project_root: &str,
|
||||||
|
model: Option<&str>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let mut text = replace_toml_table_block(
|
let mut text = existing.unwrap_or_default().to_owned();
|
||||||
existing.unwrap_or_default(),
|
if let Some(model) = model {
|
||||||
"mcp_servers.idea",
|
text = set_top_level_toml_value(&text, "model", model);
|
||||||
mcp_declaration.trim_end(),
|
}
|
||||||
);
|
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, run_dir);
|
||||||
text = ensure_codex_project_trust(&text, project_root);
|
text = ensure_codex_project_trust(&text, project_root);
|
||||||
if !text.ends_with('\n') {
|
if !text.ends_with('\n') {
|
||||||
@ -525,6 +527,33 @@ fn codex_config_toml(
|
|||||||
text
|
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 {
|
fn replace_toml_table_block(existing: &str, table: &str, replacement: &str) -> String {
|
||||||
let header = format!("[{table}]");
|
let header = format!("[{table}]");
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
|
|||||||
@ -228,9 +228,6 @@ mod tests {
|
|||||||
project_root: root,
|
project_root: root,
|
||||||
run_dir,
|
run_dir,
|
||||||
model: None,
|
model: None,
|
||||||
model_provider: None,
|
|
||||||
model_provider_base_url: None,
|
|
||||||
model_provider_env_key: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,9 +282,6 @@ mod tests {
|
|||||||
project_root: "/proj",
|
project_root: "/proj",
|
||||||
run_dir: "/run",
|
run_dir: "/run",
|
||||||
model: Some("claude-sonnet-4-5"),
|
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);
|
let proj = ClaudePermissionProjector.project(None, None, &ctx);
|
||||||
assert!(proj.args.is_empty());
|
assert!(proj.args.is_empty());
|
||||||
|
|||||||
@ -133,49 +133,17 @@ fn codex_managed_keys(ctx: &ProjectionContext, include_permissions: bool) -> Vec
|
|||||||
if ctx.model.is_some() {
|
if ctx.model.is_some() {
|
||||||
keys.push("model".to_owned());
|
keys.push("model".to_owned());
|
||||||
}
|
}
|
||||||
if ctx.model_provider.is_some() {
|
|
||||||
keys.push("model_provider".to_owned());
|
|
||||||
}
|
|
||||||
keys
|
keys
|
||||||
}
|
}
|
||||||
|
|
||||||
fn codex_managed_tables(ctx: &ProjectionContext) -> Vec<String> {
|
fn codex_managed_tables(_ctx: &ProjectionContext) -> Vec<String> {
|
||||||
let mut tables = vec![SANDBOX_WORKSPACE_WRITE_TABLE.to_owned()];
|
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 append_codex_model_config(contents: &mut String, ctx: &ProjectionContext) {
|
fn append_codex_model_config(contents: &mut String, ctx: &ProjectionContext) {
|
||||||
if let Some(model) = ctx.model {
|
if let Some(model) = ctx.model {
|
||||||
contents.push_str(&format!("model = {}\n", toml_string(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<NetworkPolicy>) -> Vec<(String, String)> {
|
fn codex_network_env(network: Option<NetworkPolicy>) -> Vec<(String, String)> {
|
||||||
@ -221,9 +189,6 @@ mod tests {
|
|||||||
project_root: "/proj",
|
project_root: "/proj",
|
||||||
run_dir: "/run/agent",
|
run_dir: "/run/agent",
|
||||||
model: None,
|
model: None,
|
||||||
model_provider: None,
|
|
||||||
model_provider_base_url: None,
|
|
||||||
model_provider_env_key: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,14 +238,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn model_projection_without_permissions_writes_model_provider_and_custom_table() {
|
fn model_projection_without_permissions_writes_only_model() {
|
||||||
let ctx = ProjectionContext {
|
let ctx = ProjectionContext {
|
||||||
project_root: "/proj",
|
project_root: "/proj",
|
||||||
run_dir: "/run/agent",
|
run_dir: "/run/agent",
|
||||||
model: Some("gpt-5"),
|
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);
|
let proj = CodexPermissionProjector.project(None, None, &ctx);
|
||||||
assert!(proj.args.is_empty());
|
assert!(proj.args.is_empty());
|
||||||
@ -292,27 +254,16 @@ mod tests {
|
|||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
assert!(managed_keys.contains(&"model".to_owned()));
|
assert!(managed_keys.contains(&"model".to_owned()));
|
||||||
assert!(managed_keys.contains(&"model_provider".to_owned()));
|
assert!(!managed_keys.contains(&"model_provider".to_owned()));
|
||||||
assert!(
|
assert_eq!(
|
||||||
managed_tables.contains(&"model_providers.\"openai-compatible\"".to_owned())
|
managed_tables,
|
||||||
|
&vec![SANDBOX_WORKSPACE_WRITE_TABLE.to_owned()]
|
||||||
);
|
);
|
||||||
assert!(contents.contains("model = \"gpt-5\""), "{contents}");
|
assert!(contents.contains("model = \"gpt-5\""), "{contents}");
|
||||||
assert!(
|
assert!(!contents.contains("model_provider"), "{contents}");
|
||||||
contents.contains("model_provider = \"openai-compatible\""),
|
assert!(!contents.contains("model_providers"), "{contents}");
|
||||||
"{contents}"
|
assert!(!contents.contains("base_url"), "{contents}");
|
||||||
);
|
assert!(!contents.contains("env_key"), "{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}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
ProjectedFile::Replace { .. } => panic!("Codex must emit a MergeToml file"),
|
ProjectedFile::Replace { .. } => panic!("Codex must emit a MergeToml file"),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,29 +69,28 @@ use backend::dto::{
|
|||||||
parse_sprint_id_dto, parse_sprint_status_dto, parse_task_id, parse_template_id,
|
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,
|
sort_ticket_rows, sprint_version_dto, update_input, version_dto, AgentDriftListDto, AgentDto,
|
||||||
AgentListDto, AssignSkillRequestDto, AttachLiveAgentRequestDto, AttachLiveAgentResponseDto,
|
AgentListDto, AssignSkillRequestDto, AttachLiveAgentRequestDto, AttachLiveAgentResponseDto,
|
||||||
BackgroundTaskDto, ChangeAgentProfileDto, ChangeAgentProfileRequestDto, ClaudeProviderListDto,
|
BackgroundTaskDto, ChangeAgentProfileDto, ChangeAgentProfileRequestDto,
|
||||||
CloneOpenCodeProfileFromSeedRequestDto, CodexProviderListDto, ConfigureProfilesRequestDto,
|
CloneOpenCodeProfileFromSeedRequestDto, ConfigureProfilesRequestDto, ConversationDetailsDto,
|
||||||
ConversationDetailsDto, CreateAgentFromTemplateRequestDto, CreateAgentRequestDto,
|
CreateAgentFromTemplateRequestDto, CreateAgentRequestDto, CreateMemoryRequestDto,
|
||||||
CreateMemoryRequestDto, CreateSkillRequestDto, CreateTemplateRequestDto,
|
CreateSkillRequestDto, CreateTemplateRequestDto, DetectProfilesRequestDto,
|
||||||
DetectProfilesRequestDto, DetectProfilesResponseDto, EffectivePermissionsDto,
|
DetectProfilesResponseDto, EffectivePermissionsDto, EmbedderEnginesDto, EmbedderProfileDto,
|
||||||
EmbedderEnginesDto, EmbedderProfileDto, EmbedderProfileListDto, ErrorDto, FirstRunStateDto,
|
EmbedderProfileListDto, ErrorDto, FirstRunStateDto, GitBranchesDto, GitCheckoutRequestDto,
|
||||||
GitBranchesDto, GitCheckoutRequestDto, GitCommitDto, GitCommitListDto, GitCommitRequestDto,
|
GitCommitDto, GitCommitListDto, GitCommitRequestDto, GitStageRequestDto, GitStatusListDto,
|
||||||
GitStageRequestDto, GitStatusListDto, GraphCommitListDto, HealthRequestDto, HealthResponseDto,
|
GraphCommitListDto, HealthRequestDto, HealthResponseDto, InspectConversationRequestDto,
|
||||||
InspectConversationRequestDto, LaunchAgentRequestDto, LiveAgentListDto, MemoryDto,
|
LaunchAgentRequestDto, LiveAgentListDto, MemoryDto, MemoryIndexDto, MemoryLinksDto,
|
||||||
MemoryIndexDto, MemoryLinksDto, MemoryListDto, OpenCodeProviderListDto, OpenTerminalRequestDto,
|
MemoryListDto, OpenCodeProviderListDto, OpenTerminalRequestDto, ProfileDto, ProfileListDto,
|
||||||
ProfileDto, ProfileListDto, ProjectDto, ProjectListDto, ProjectMcpToolPermissionsDto,
|
ProjectDto, ProjectListDto, ProjectMcpToolPermissionsDto, ProjectPermissionsDto,
|
||||||
ProjectPermissionsDto, ProjectSystemPermissionsDto, ProjectWorkStateDto,
|
ProjectSystemPermissionsDto, ProjectWorkStateDto, ReadAgentContextResponseDto,
|
||||||
ReadAgentContextResponseDto, ReadConversationPageRequestDto, RecallMemoryRequestDto,
|
ReadConversationPageRequestDto, RecallMemoryRequestDto, ResolveAgentPermissionsRequestDto,
|
||||||
ResolveAgentPermissionsRequestDto, ResolveAgentSystemPermissionsRequestDto,
|
ResolveAgentSystemPermissionsRequestDto, ResolvedAgentSystemPermissionsDto,
|
||||||
ResolvedAgentSystemPermissionsDto, ResumableAgentListDto, SaveClaudeProviderProfileRequestDto,
|
ResumableAgentListDto, SaveEmbedderProfileRequestDto, SaveOpenCodeProviderProfileRequestDto,
|
||||||
SaveCodexProviderProfileRequestDto, SaveEmbedderProfileRequestDto,
|
SaveProfileRequestDto, SkillDto, SkillListDto, SprintCreateRequestDto, SprintDeleteRequestDto,
|
||||||
SaveOpenCodeProviderProfileRequestDto, SaveProfileRequestDto, SkillDto, SkillListDto,
|
SprintDto, SprintListDto, SprintListRequestDto, SprintRenameRequestDto,
|
||||||
SprintCreateRequestDto, SprintDeleteRequestDto, SprintDto, SprintListDto, SprintListRequestDto,
|
SprintReorderRequestDto, StopLiveAgentRequestDto, StopLiveAgentResponseDto,
|
||||||
SprintRenameRequestDto, SprintReorderRequestDto, StopLiveAgentRequestDto,
|
SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto,
|
||||||
StopLiveAgentResponseDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto,
|
TerminalSessionDto, TicketAssignRequestDto, TicketCarnetDto, TicketCreateRequestDto,
|
||||||
TemplateListDto, TerminalSessionDto, TicketAssignRequestDto, TicketCarnetDto,
|
TicketDeleteRequestDto, TicketDto, TicketLinkCommandRequestDto, TicketListPageInput,
|
||||||
TicketCreateRequestDto, TicketDeleteRequestDto, TicketDto, TicketLinkCommandRequestDto,
|
TicketListRequestDto, TicketReadRequestDto, TicketSprintAssignRequestDto,
|
||||||
TicketListPageInput, TicketListRequestDto, TicketReadRequestDto, TicketSprintAssignRequestDto,
|
|
||||||
TicketSprintUnassignRequestDto, TicketUnlinkCommandRequestDto, TicketUpdateCarnetRequestDto,
|
TicketSprintUnassignRequestDto, TicketUnlinkCommandRequestDto, TicketUpdateCarnetRequestDto,
|
||||||
TicketUpdateRequestDto, TurnPageDto, UnassignSkillRequestDto, UpdateAgentContextRequestDto,
|
TicketUpdateRequestDto, TurnPageDto, UnassignSkillRequestDto, UpdateAgentContextRequestDto,
|
||||||
UpdateAgentMcpToolPermissionsRequestDto, UpdateAgentPermissionsRequestDto,
|
UpdateAgentMcpToolPermissionsRequestDto, UpdateAgentPermissionsRequestDto,
|
||||||
@ -2357,14 +2356,6 @@ async fn invoke(
|
|||||||
"save_opencode_provider_profile" => {
|
"save_opencode_provider_profile" => {
|
||||||
invoke_save_opencode_provider_profile(&request.args, &state.app).await
|
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,
|
"delete_profile" => invoke_delete_profile(&request.args, &state.app).await,
|
||||||
"configure_profiles" => invoke_configure_profiles(&request.args, &state.app).await,
|
"configure_profiles" => invoke_configure_profiles(&request.args, &state.app).await,
|
||||||
"clone_opencode_profile_from_seed" => {
|
"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)
|
serde_json::to_value(output).map_err(serialization_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn invoke_list_codex_providers(state: &BackendCore) -> Result<Value, ErrorDto> {
|
|
||||||
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<Value, ErrorDto> {
|
|
||||||
let request = required_request::<SaveCodexProviderProfileRequestDto>(
|
|
||||||
"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<Value, ErrorDto> {
|
|
||||||
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<Value, ErrorDto> {
|
|
||||||
let request = required_request::<SaveClaudeProviderProfileRequestDto>(
|
|
||||||
"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<Value, ErrorDto> {
|
async fn invoke_delete_profile(args: &Value, state: &BackendCore) -> Result<Value, ErrorDto> {
|
||||||
let profile_id = string_arg(args, "profileId", "delete_profile")?;
|
let profile_id = string_arg(args, "profileId", "delete_profile")?;
|
||||||
let input = backend::dto::parse_delete_profile(profile_id)?;
|
let input = backend::dto::parse_delete_profile(profile_id)?;
|
||||||
@ -7707,10 +7654,6 @@ mod tests {
|
|||||||
"save_profile",
|
"save_profile",
|
||||||
"list_opencode_providers",
|
"list_opencode_providers",
|
||||||
"save_opencode_provider_profile",
|
"save_opencode_provider_profile",
|
||||||
"list_codex_providers",
|
|
||||||
"save_codex_provider_profile",
|
|
||||||
"list_claude_providers",
|
|
||||||
"save_claude_provider_profile",
|
|
||||||
"delete_profile",
|
"delete_profile",
|
||||||
"configure_profiles",
|
"configure_profiles",
|
||||||
"clone_opencode_profile_from_seed",
|
"clone_opencode_profile_from_seed",
|
||||||
|
|||||||
@ -15,8 +15,6 @@ import type {
|
|||||||
Agent,
|
Agent,
|
||||||
AgentDrift,
|
AgentDrift,
|
||||||
AgentProfile,
|
AgentProfile,
|
||||||
ClaudeProviderCatalogEntry,
|
|
||||||
CodexProviderCatalogEntry,
|
|
||||||
EffectivePermissions,
|
EffectivePermissions,
|
||||||
EmbedderEngines,
|
EmbedderEngines,
|
||||||
EmbedderProfile,
|
EmbedderProfile,
|
||||||
@ -67,8 +65,6 @@ import type {
|
|||||||
PermissionGateway,
|
PermissionGateway,
|
||||||
ProfileGateway,
|
ProfileGateway,
|
||||||
ProjectGateway,
|
ProjectGateway,
|
||||||
SaveClaudeProviderProfileInput,
|
|
||||||
SaveCodexProviderProfileInput,
|
|
||||||
SaveOpenCodeProviderProfileInput,
|
SaveOpenCodeProviderProfileInput,
|
||||||
SkillGateway,
|
SkillGateway,
|
||||||
TemplateGateway,
|
TemplateGateway,
|
||||||
@ -206,37 +202,6 @@ export class HttpProfileGateway implements ProfileGateway {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
listCodexProviders(): Promise<CodexProviderCatalogEntry[]> {
|
|
||||||
return this.http.invoke<CodexProviderCatalogEntry[]>("list_codex_providers");
|
|
||||||
}
|
|
||||||
saveCodexProviderProfile(
|
|
||||||
input: SaveCodexProviderProfileInput,
|
|
||||||
): Promise<AgentProfile> {
|
|
||||||
return this.http.invoke<AgentProfile>("save_codex_provider_profile", {
|
|
||||||
request: {
|
|
||||||
profile: input.profile,
|
|
||||||
providerId: input.providerId,
|
|
||||||
model: input.model,
|
|
||||||
apiKey: input.apiKey,
|
|
||||||
custom: input.custom,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
listClaudeProviders(): Promise<ClaudeProviderCatalogEntry[]> {
|
|
||||||
return this.http.invoke<ClaudeProviderCatalogEntry[]>("list_claude_providers");
|
|
||||||
}
|
|
||||||
saveClaudeProviderProfile(
|
|
||||||
input: SaveClaudeProviderProfileInput,
|
|
||||||
): Promise<AgentProfile> {
|
|
||||||
return this.http.invoke<AgentProfile>("save_claude_provider_profile", {
|
|
||||||
request: {
|
|
||||||
profile: input.profile,
|
|
||||||
providerId: input.providerId,
|
|
||||||
model: input.model,
|
|
||||||
apiKey: input.apiKey,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HttpModelServerGateway implements ModelServerGateway {
|
export class HttpModelServerGateway implements ModelServerGateway {
|
||||||
|
|||||||
@ -9,8 +9,6 @@ import type {
|
|||||||
AgentDrift,
|
AgentDrift,
|
||||||
AppExitWorkGuardState,
|
AppExitWorkGuardState,
|
||||||
AgentProfile,
|
AgentProfile,
|
||||||
ClaudeProviderCatalogEntry,
|
|
||||||
CodexProviderCatalogEntry,
|
|
||||||
DiagnosticWarning,
|
DiagnosticWarning,
|
||||||
DomainEvent,
|
DomainEvent,
|
||||||
EmbedderEngines,
|
EmbedderEngines,
|
||||||
@ -105,8 +103,6 @@ import type {
|
|||||||
ReattachResult,
|
ReattachResult,
|
||||||
RemoteGateway,
|
RemoteGateway,
|
||||||
ReviewPluginPackageInput,
|
ReviewPluginPackageInput,
|
||||||
SaveClaudeProviderProfileInput,
|
|
||||||
SaveCodexProviderProfileInput,
|
|
||||||
SaveOpenCodeProviderProfileInput,
|
SaveOpenCodeProviderProfileInput,
|
||||||
SkillGateway,
|
SkillGateway,
|
||||||
StoppedLiveAgent,
|
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
|
* 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
|
* 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);
|
return structuredClone(saved);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listCodexProviders(): Promise<CodexProviderCatalogEntry[]> {
|
|
||||||
return structuredClone(MOCK_CODEX_PROVIDERS);
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveCodexProviderProfile(
|
|
||||||
input: SaveCodexProviderProfileInput,
|
|
||||||
): Promise<AgentProfile> {
|
|
||||||
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<ClaudeProviderCatalogEntry[]> {
|
|
||||||
return structuredClone(MOCK_CLAUDE_PROVIDERS);
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveClaudeProviderProfile(
|
|
||||||
input: SaveClaudeProviderProfileInput,
|
|
||||||
): Promise<AgentProfile> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -10,8 +10,6 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
AgentProfile,
|
AgentProfile,
|
||||||
ClaudeProviderCatalogEntry,
|
|
||||||
CodexProviderCatalogEntry,
|
|
||||||
FirstRunState,
|
FirstRunState,
|
||||||
OpenCodeProviderCatalogEntry,
|
OpenCodeProviderCatalogEntry,
|
||||||
ProfileAvailability,
|
ProfileAvailability,
|
||||||
@ -19,8 +17,6 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
CloneOpenCodeProfileFromSeedInput,
|
CloneOpenCodeProfileFromSeedInput,
|
||||||
ProfileGateway,
|
ProfileGateway,
|
||||||
SaveClaudeProviderProfileInput,
|
|
||||||
SaveCodexProviderProfileInput,
|
|
||||||
SaveOpenCodeProviderProfileInput,
|
SaveOpenCodeProviderProfileInput,
|
||||||
} from "@/ports";
|
} from "@/ports";
|
||||||
|
|
||||||
@ -83,38 +79,4 @@ export class TauriProfileGateway implements ProfileGateway {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
listCodexProviders(): Promise<CodexProviderCatalogEntry[]> {
|
|
||||||
return invoke<CodexProviderCatalogEntry[]>("list_codex_providers");
|
|
||||||
}
|
|
||||||
|
|
||||||
saveCodexProviderProfile(
|
|
||||||
input: SaveCodexProviderProfileInput,
|
|
||||||
): Promise<AgentProfile> {
|
|
||||||
return invoke<AgentProfile>("save_codex_provider_profile", {
|
|
||||||
request: {
|
|
||||||
profile: input.profile,
|
|
||||||
providerId: input.providerId,
|
|
||||||
model: input.model,
|
|
||||||
apiKey: input.apiKey,
|
|
||||||
custom: input.custom,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
listClaudeProviders(): Promise<ClaudeProviderCatalogEntry[]> {
|
|
||||||
return invoke<ClaudeProviderCatalogEntry[]>("list_claude_providers");
|
|
||||||
}
|
|
||||||
|
|
||||||
saveClaudeProviderProfile(
|
|
||||||
input: SaveClaudeProviderProfileInput,
|
|
||||||
): Promise<AgentProfile> {
|
|
||||||
return invoke<AgentProfile>("save_claude_provider_profile", {
|
|
||||||
request: {
|
|
||||||
profile: input.profile,
|
|
||||||
providerId: input.providerId,
|
|
||||||
model: input.model,
|
|
||||||
apiKey: input.apiKey,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1068,36 +1068,6 @@ export interface OpenCodeProviderConfig {
|
|||||||
custom?: CustomProviderConfig;
|
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
|
* Config for a custom OpenCode provider (mirror of the backend
|
||||||
* `CustomProviderConfig`, camelCase wire format), carried by
|
* `CustomProviderConfig`, camelCase wire format), carried by
|
||||||
@ -1112,18 +1082,6 @@ export interface CustomProviderConfig {
|
|||||||
displayName?: string;
|
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
|
* One entry of the static OpenCode cloud-provider catalogue (mirror of the
|
||||||
* backend `OpenCodeProviderDto`), returned by
|
* backend `OpenCodeProviderDto`), returned by
|
||||||
@ -1138,28 +1096,6 @@ export interface OpenCodeProviderCatalogEntry {
|
|||||||
models: string[];
|
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
|
* A declarative AI-CLI profile (mirror of the backend `AgentProfile`). `id` is a
|
||||||
* UUID string; `detect` is the optional detection command line.
|
* UUID string; `detect` is the optional detection command line.
|
||||||
@ -1196,10 +1132,11 @@ export interface AgentProfile {
|
|||||||
* both.
|
* both.
|
||||||
*/
|
*/
|
||||||
opencodeProvider?: OpenCodeProviderConfig;
|
opencodeProvider?: OpenCodeProviderConfig;
|
||||||
/** Codex provider/model config (ticket #99). */
|
/**
|
||||||
codexProvider?: CodexProviderConfig;
|
* Optional direct CLI model setting for Codex/Claude. `undefined` keeps the
|
||||||
/** Claude provider/model config (ticket #99). */
|
* CLI's own default. OpenCode keeps its dedicated provider/local model fields.
|
||||||
claudeProvider?: ClaudeProviderConfig;
|
*/
|
||||||
|
model?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Availability of a candidate profile after detection (mirror of the DTO). */
|
/** Availability of a candidate profile after detection (mirror of the DTO). */
|
||||||
|
|||||||
@ -183,47 +183,41 @@ describe("FirstRunWizard (with MockProfileGateway)", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("FirstRunWizard — Codex/Claude provider configuration (ticket #99)", () => {
|
describe("FirstRunWizard — Codex/Claude model configuration (ticket #99)", () => {
|
||||||
it("saves a Codex provider profile and clears the literal key", async () => {
|
it("shows only a free-form model field for Codex and persists no provider/API key", async () => {
|
||||||
const { profile } = renderWizard();
|
const { profile } = renderWizard();
|
||||||
await waitForLoaded();
|
await waitForLoaded();
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
(screen.getByLabelText("use Claude Code") as HTMLInputElement).checked,
|
||||||
|
).toBe(true),
|
||||||
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByLabelText("use OpenAI Codex CLI"));
|
fireEvent.click(screen.getByLabelText("use OpenAI Codex CLI"));
|
||||||
|
|
||||||
const row = within(
|
const row = within(
|
||||||
screen.getByLabelText("use OpenAI Codex CLI").closest("li")!,
|
screen.getByLabelText("use OpenAI Codex CLI").closest("li")!,
|
||||||
);
|
);
|
||||||
const providerSelect = await row.findByLabelText("OpenAI Codex CLI provider");
|
expect(row.queryByLabelText("OpenAI Codex CLI provider")).toBeNull();
|
||||||
const modelSelect = row.getByLabelText(
|
expect(row.queryByLabelText("OpenAI Codex CLI provider search")).toBeNull();
|
||||||
"OpenAI Codex CLI model",
|
expect(row.queryByLabelText("OpenAI Codex CLI api key")).toBeNull();
|
||||||
) as HTMLSelectElement;
|
|
||||||
expect(modelSelect.disabled).toBe(true);
|
|
||||||
|
|
||||||
fireEvent.change(providerSelect, { target: { value: "openai" } });
|
fireEvent.change(row.getByLabelText("OpenAI Codex CLI model"), {
|
||||||
expect(modelSelect.value).toBe("");
|
target: { value: "gpt-5-codex" },
|
||||||
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.click(row.getByRole("button", { name: "Enregistrer Codex" }));
|
fireEvent.click(screen.getByRole("button", { name: "Save and continue" }));
|
||||||
|
|
||||||
await waitFor(async () => {
|
await waitFor(async () => {
|
||||||
const saved = await profile.listProfiles();
|
const saved = await profile.listProfiles();
|
||||||
const codex = saved.find((p) => p.command === "codex");
|
const codex = saved.find((p) => p.command === "codex");
|
||||||
expect(codex?.codexProvider).toEqual({
|
expect(codex?.model).toBe("gpt-5-codex");
|
||||||
providerId: "openai",
|
expect(JSON.stringify(codex)).not.toContain("provider");
|
||||||
model: "gpt-5-codex",
|
expect(JSON.stringify(codex)).not.toContain("apiKey");
|
||||||
apiKeyRef: "mock-secret-mock-codex",
|
|
||||||
custom: undefined,
|
|
||||||
});
|
});
|
||||||
expect(JSON.stringify(codex)).not.toContain("sk-codex-secret");
|
|
||||||
});
|
|
||||||
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();
|
const { profile } = renderWizard();
|
||||||
await waitForLoaded();
|
await waitForLoaded();
|
||||||
|
|
||||||
@ -233,31 +227,26 @@ describe("FirstRunWizard — Codex/Claude provider configuration (ticket #99)",
|
|||||||
if (!claudeToggle.checked) fireEvent.click(claudeToggle);
|
if (!claudeToggle.checked) fireEvent.click(claudeToggle);
|
||||||
|
|
||||||
const row = within(claudeToggle.closest("li")!);
|
const row = within(claudeToggle.closest("li")!);
|
||||||
fireEvent.change(await row.findByLabelText("Claude Code provider"), {
|
expect(row.queryByLabelText("Claude Code provider")).toBeNull();
|
||||||
target: { value: "anthropic" },
|
expect(row.queryByLabelText("Claude Code provider search")).toBeNull();
|
||||||
});
|
expect(row.queryByLabelText("Claude Code api key")).toBeNull();
|
||||||
|
|
||||||
fireEvent.change(row.getByLabelText("Claude Code model"), {
|
fireEvent.change(row.getByLabelText("Claude Code model"), {
|
||||||
target: { value: "claude-sonnet-4-5" },
|
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 () => {
|
await waitFor(async () => {
|
||||||
const saved = await profile.listProfiles();
|
const saved = await profile.listProfiles();
|
||||||
const claude = saved.find((p) => p.command === "claude");
|
const claude = saved.find((p) => p.command === "claude");
|
||||||
expect(claude?.claudeProvider).toEqual({
|
expect(claude?.model).toBe("claude-sonnet-4-5");
|
||||||
providerId: "anthropic",
|
expect(JSON.stringify(claude)).not.toContain("provider");
|
||||||
model: "claude-sonnet-4-5",
|
expect(JSON.stringify(claude)).not.toContain("apiKey");
|
||||||
apiKeyRef: "mock-secret-mock-claude",
|
|
||||||
});
|
});
|
||||||
expect(JSON.stringify(claude)).not.toContain("sk-claude-secret");
|
|
||||||
});
|
|
||||||
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();
|
const profile = new MockProfileGateway();
|
||||||
await profile.configureProfiles([
|
await profile.configureProfiles([
|
||||||
{
|
{
|
||||||
@ -269,11 +258,7 @@ describe("FirstRunWizard — Codex/Claude provider configuration (ticket #99)",
|
|||||||
detect: "codex --version",
|
detect: "codex --version",
|
||||||
cwdTemplate: "{projectRoot}",
|
cwdTemplate: "{projectRoot}",
|
||||||
structuredAdapter: "codex",
|
structuredAdapter: "codex",
|
||||||
codexProvider: {
|
|
||||||
providerId: "openai",
|
|
||||||
model: "gpt-5-mini",
|
model: "gpt-5-mini",
|
||||||
apiKeyRef: "existing-secret-ref",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const gateways = {
|
const gateways = {
|
||||||
@ -290,20 +275,22 @@ describe("FirstRunWizard — Codex/Claude provider configuration (ticket #99)",
|
|||||||
const row = within(
|
const row = within(
|
||||||
screen.getByLabelText("use Codex configured").closest("li")!,
|
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"), {
|
fireEvent.change(row.getByLabelText("Codex configured model"), {
|
||||||
target: { value: "gpt-5" },
|
target: { value: "gpt-5" },
|
||||||
});
|
});
|
||||||
fireEvent.change(row.getByLabelText("Codex configured api key"), {
|
fireEvent.click(screen.getByRole("button", { name: "Save and continue" }));
|
||||||
target: { value: "sk-new-secret" },
|
|
||||||
});
|
|
||||||
fireEvent.click(row.getByRole("button", { name: "Enregistrer Codex" }));
|
|
||||||
|
|
||||||
await waitFor(async () => {
|
await waitFor(async () => {
|
||||||
const [saved] = await profile.listProfiles();
|
const [saved] = await profile.listProfiles();
|
||||||
expect(saved.codexProvider?.model).toBe("gpt-5");
|
expect(saved.model).toBe("gpt-5");
|
||||||
expect(saved.codexProvider?.apiKeyRef).toBe("existing-secret-ref");
|
expect(JSON.stringify(saved)).not.toContain("provider");
|
||||||
expect(JSON.stringify(saved)).not.toContain("sk-new-secret");
|
expect(JSON.stringify(saved)).not.toContain("apiKey");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -19,9 +19,6 @@ import { useCallback, useEffect, useState } from "react";
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
AgentProfile,
|
AgentProfile,
|
||||||
ClaudeProviderCatalogEntry,
|
|
||||||
CodexCustomProviderConfig,
|
|
||||||
CodexProviderCatalogEntry,
|
|
||||||
GatewayError,
|
GatewayError,
|
||||||
HttpChatConfig,
|
HttpChatConfig,
|
||||||
LocalModelServerConfig,
|
LocalModelServerConfig,
|
||||||
@ -64,20 +61,6 @@ interface OpenCodeProviderCatalog {
|
|||||||
reload: () => void;
|
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
|
* Loads the static OpenCode cloud-provider catalogue once for the whole
|
||||||
* wizard (every Cloud row shares it), so the provider/model pickers can be
|
* 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() };
|
return { providers, loading, error, reload: () => void load() };
|
||||||
}
|
}
|
||||||
|
|
||||||
function useCodexProviderCatalog(): CodexProviderCatalog {
|
|
||||||
const { profile } = useGateways();
|
|
||||||
const [providers, setProviders] = useState<CodexProviderCatalogEntry[] | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(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<ClaudeProviderCatalogEntry[] | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(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
|
* 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
|
* 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 vm = useFirstRun(forceOpen ? "edit" : "firstRun");
|
||||||
const modelServers = useModelServers();
|
const modelServers = useModelServers();
|
||||||
const providerCatalog = useOpenCodeProviderCatalog();
|
const providerCatalog = useOpenCodeProviderCatalog();
|
||||||
const codexProviderCatalog = useCodexProviderCatalog();
|
|
||||||
const claudeProviderCatalog = useClaudeProviderCatalog();
|
|
||||||
|
|
||||||
if (vm.isFirstRun === null) return null;
|
if (vm.isFirstRun === null) return null;
|
||||||
if (!forceOpen && vm.isFirstRun === false) return null;
|
if (!forceOpen && vm.isFirstRun === false) return null;
|
||||||
@ -259,8 +184,6 @@ export function FirstRunWizard({
|
|||||||
entry={entry}
|
entry={entry}
|
||||||
servers={modelServers.servers}
|
servers={modelServers.servers}
|
||||||
providerCatalog={providerCatalog}
|
providerCatalog={providerCatalog}
|
||||||
codexProviderCatalog={codexProviderCatalog}
|
|
||||||
claudeProviderCatalog={claudeProviderCatalog}
|
|
||||||
onToggle={() => vm.toggle(entry.profile.id)}
|
onToggle={() => vm.toggle(entry.profile.id)}
|
||||||
onChange={(p) => vm.updateProfile(entry.profile.id, p)}
|
onChange={(p) => vm.updateProfile(entry.profile.id, p)}
|
||||||
onRemove={() => vm.remove(entry.profile.id)}
|
onRemove={() => vm.remove(entry.profile.id)}
|
||||||
@ -292,8 +215,6 @@ function ProfileRow({
|
|||||||
entry,
|
entry,
|
||||||
servers,
|
servers,
|
||||||
providerCatalog,
|
providerCatalog,
|
||||||
codexProviderCatalog,
|
|
||||||
claudeProviderCatalog,
|
|
||||||
onToggle,
|
onToggle,
|
||||||
onChange,
|
onChange,
|
||||||
onRemove,
|
onRemove,
|
||||||
@ -304,8 +225,6 @@ function ProfileRow({
|
|||||||
servers: LocalModelServerConfig[];
|
servers: LocalModelServerConfig[];
|
||||||
/** OpenCode cloud-provider catalogue (ticket #92), shared across rows. */
|
/** OpenCode cloud-provider catalogue (ticket #92), shared across rows. */
|
||||||
providerCatalog: OpenCodeProviderCatalog;
|
providerCatalog: OpenCodeProviderCatalog;
|
||||||
codexProviderCatalog: CodexProviderCatalog;
|
|
||||||
claudeProviderCatalog: ClaudeProviderCatalog;
|
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
onChange: (p: AgentProfile) => void;
|
onChange: (p: AgentProfile) => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
@ -411,22 +330,10 @@ function ProfileRow({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{profile.structuredAdapter === "codex" &&
|
{(profile.structuredAdapter === "codex" ||
|
||||||
(selected || profile.codexProvider) && (
|
profile.structuredAdapter === "claude") &&
|
||||||
<CodexProviderFields
|
(selected || profile.model) && (
|
||||||
profile={profile}
|
<CliModelField profile={profile} onChange={onChange} />
|
||||||
catalog={codexProviderCatalog}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{profile.structuredAdapter === "claude" &&
|
|
||||||
(selected || profile.claudeProvider) && (
|
|
||||||
<ClaudeProviderFields
|
|
||||||
profile={profile}
|
|
||||||
catalog={claudeProviderCatalog}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@ -893,430 +800,32 @@ function OpenCodeProviderFields({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type SimpleProviderCatalogEntry =
|
function CliModelField({
|
||||||
| 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<AgentProfile>;
|
|
||||||
onChange: (p: AgentProfile) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProviderModelSecretFields({
|
|
||||||
engine,
|
|
||||||
profile,
|
profile,
|
||||||
catalog,
|
|
||||||
existing,
|
|
||||||
customSupported,
|
|
||||||
saveProfile,
|
|
||||||
onChange,
|
onChange,
|
||||||
}: ProviderModelSecretFieldsProps) {
|
}: {
|
||||||
const [mode, setMode] = useState<"catalog" | "custom">(
|
profile: AgentProfile;
|
||||||
existing && "custom" in existing && existing.custom ? "custom" : "catalog",
|
onChange: (p: AgentProfile) => void;
|
||||||
);
|
}) {
|
||||||
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<string | null>(null);
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<CloudFieldErrors>({});
|
|
||||||
|
|
||||||
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 (
|
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 (
|
|
||||||
<fieldset className="mt-1 flex flex-col gap-2 rounded-md border border-border/70 p-2">
|
|
||||||
<legend className="px-1 text-xs font-medium text-muted">
|
|
||||||
Provider cloud ({engine})
|
|
||||||
</legend>
|
|
||||||
|
|
||||||
{saveError && (
|
|
||||||
<p role="alert" className="text-sm text-danger">
|
|
||||||
{saveError}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{catalog.loading && (
|
|
||||||
<p className="text-xs text-faint">Chargement des providers…</p>
|
|
||||||
)}
|
|
||||||
{catalog.error && (
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<p role="alert" className="text-sm text-danger">
|
|
||||||
Impossible de charger la liste des providers cloud.
|
|
||||||
</p>
|
|
||||||
<Button size="sm" onClick={() => catalog.reload()} className="w-fit">
|
|
||||||
Réessayer
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{mode === "catalog" && (
|
|
||||||
<>
|
|
||||||
<label className="flex flex-col gap-1">
|
|
||||||
<Caption>Provider</Caption>
|
|
||||||
{catalogReady && (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
aria-label={`${profile.name} provider search`}
|
|
||||||
placeholder="Rechercher un provider…"
|
|
||||||
value={providerFilter}
|
|
||||||
onChange={(e) => setProviderFilter(e.target.value)}
|
|
||||||
className="h-8 w-full rounded-md border border-border bg-raised px-3 text-xs text-content outline-none"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<select
|
|
||||||
aria-label={`${profile.name} provider`}
|
|
||||||
value={providerId}
|
|
||||||
disabled={!catalogReady}
|
|
||||||
onChange={(e) => {
|
|
||||||
const v = e.target.value;
|
|
||||||
if (v === CUSTOM_PROVIDER_VALUE) {
|
|
||||||
setMode("custom");
|
|
||||||
setProviderId("");
|
|
||||||
} else {
|
|
||||||
setProviderId(v);
|
|
||||||
}
|
|
||||||
setModel("");
|
|
||||||
setFieldErrors((prev) => ({
|
|
||||||
...prev,
|
|
||||||
providerId: undefined,
|
|
||||||
model: undefined,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"h-9 w-full rounded-md border bg-raised px-3 text-sm text-content outline-none",
|
|
||||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
fieldErrors.providerId ? "border-danger" : "border-border",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<option value="" disabled>
|
|
||||||
{catalog.loading ? "Chargement des providers…" : "Choisir un provider…"}
|
|
||||||
</option>
|
|
||||||
{filteredProviders.map((p) => (
|
|
||||||
<option key={p.providerId} value={p.providerId}>
|
|
||||||
{p.displayName}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
{customSupported && (
|
|
||||||
<option value={CUSTOM_PROVIDER_VALUE}>Autre / personnalisé…</option>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
{fieldErrors.providerId && (
|
|
||||||
<small className="text-xs text-danger">{fieldErrors.providerId}</small>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex flex-col gap-1">
|
|
||||||
<Caption>Modèle</Caption>
|
|
||||||
<select
|
|
||||||
aria-label={`${profile.name} model`}
|
|
||||||
value={model}
|
|
||||||
disabled={providerId.length === 0}
|
|
||||||
onChange={(e) => {
|
|
||||||
setModel(e.target.value);
|
|
||||||
setFieldErrors((prev) => ({ ...prev, model: undefined }));
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"h-9 w-full rounded-md border bg-raised px-3 text-sm text-content outline-none",
|
|
||||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
fieldErrors.model ? "border-danger" : "border-border",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<option value="" disabled>
|
|
||||||
{providerId.length === 0 ? "—" : "Choisir un modèle…"}
|
|
||||||
</option>
|
|
||||||
{models.map((m) => (
|
|
||||||
<option key={m} value={m}>
|
|
||||||
{m}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{fieldErrors.model && (
|
|
||||||
<small className="text-xs text-danger">{fieldErrors.model}</small>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{mode === "custom" && customSupported && (
|
|
||||||
<fieldset className="flex flex-col gap-2 rounded-md border border-border/50 p-2">
|
|
||||||
<legend className="px-1 text-xs font-medium text-muted">
|
|
||||||
Provider personnalisé
|
|
||||||
</legend>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="w-fit"
|
|
||||||
onClick={() => {
|
|
||||||
setMode("catalog");
|
|
||||||
setProviderId("");
|
|
||||||
setModel("");
|
|
||||||
setFieldErrors({});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
← Choisir un provider du catalogue
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<label className="flex flex-col gap-1">
|
|
||||||
<Caption>Identifiant du provider</Caption>
|
|
||||||
<Input
|
|
||||||
aria-label={`${profile.name} custom provider id`}
|
|
||||||
value={providerId}
|
|
||||||
placeholder="ex. mon-provider"
|
|
||||||
invalid={Boolean(fieldErrors.providerId)}
|
|
||||||
onChange={(e) => {
|
|
||||||
setProviderId(e.target.value);
|
|
||||||
setFieldErrors((prev) => ({ ...prev, providerId: undefined }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{fieldErrors.providerId && (
|
|
||||||
<small className="text-xs text-danger">{fieldErrors.providerId}</small>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex flex-col gap-1">
|
|
||||||
<Caption>URL de base</Caption>
|
|
||||||
<Input
|
|
||||||
aria-label={`${profile.name} custom base url`}
|
|
||||||
value={customBaseUrl}
|
|
||||||
placeholder="https://api.mon-provider.example/v1"
|
|
||||||
invalid={Boolean(fieldErrors.baseUrl)}
|
|
||||||
onChange={(e) => {
|
|
||||||
setCustomBaseUrl(e.target.value);
|
|
||||||
setFieldErrors((prev) => ({ ...prev, baseUrl: undefined }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{fieldErrors.baseUrl && (
|
|
||||||
<small className="text-xs text-danger">{fieldErrors.baseUrl}</small>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex flex-col gap-1">
|
<label className="flex flex-col gap-1">
|
||||||
<Caption>Modèle</Caption>
|
<Caption>Modèle</Caption>
|
||||||
<Input
|
<Input
|
||||||
aria-label={`${profile.name} model`}
|
aria-label={`${profile.name} model`}
|
||||||
value={model}
|
value={profile.model ?? ""}
|
||||||
placeholder="ex. mon-modele-1"
|
placeholder="Laisser vide pour le modèle par défaut de la CLI"
|
||||||
invalid={Boolean(fieldErrors.model)}
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setModel(e.target.value);
|
const model = e.target.value.trim();
|
||||||
setFieldErrors((prev) => ({ ...prev, model: undefined }));
|
onChange({
|
||||||
|
...profile,
|
||||||
|
model: model.length > 0 ? model : undefined,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{fieldErrors.model && (
|
|
||||||
<small className="text-xs text-danger">{fieldErrors.model}</small>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex flex-col gap-1">
|
|
||||||
<Caption>Libellé du provider (optionnel)</Caption>
|
|
||||||
<Input
|
|
||||||
aria-label={`${profile.name} custom display name`}
|
|
||||||
value={customDisplayName}
|
|
||||||
placeholder="ex. Mon provider"
|
|
||||||
onChange={(e) => setCustomDisplayName(e.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<label className="flex flex-col gap-1">
|
|
||||||
<Caption>Clé API</Caption>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Input
|
|
||||||
aria-label={`${profile.name} api key`}
|
|
||||||
type={showKey ? "text" : "password"}
|
|
||||||
value={apiKey}
|
|
||||||
placeholder={
|
|
||||||
isEditing
|
|
||||||
? "Ressaisissez la clé API pour confirmer l'enregistrement"
|
|
||||||
: "ex. sk-…"
|
|
||||||
}
|
|
||||||
invalid={Boolean(fieldErrors.apiKey)}
|
|
||||||
onChange={(e) => {
|
|
||||||
setApiKey(e.target.value);
|
|
||||||
setFieldErrors((prev) => ({ ...prev, apiKey: undefined }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
aria-label="afficher/masquer la clé API"
|
|
||||||
onClick={() => setShowKey((v) => !v)}
|
|
||||||
>
|
|
||||||
{showKey ? "🙈" : "👁"}
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
{fieldErrors.apiKey && (
|
|
||||||
<small className="text-xs text-danger">{fieldErrors.apiKey}</small>
|
|
||||||
)}
|
|
||||||
<small className="text-xs text-faint">
|
<small className="text-xs text-faint">
|
||||||
Jamais affichée ni renvoyée par IdeA une fois enregistrée ; stockée
|
Optionnel. L'authentification Codex/Claude reste gérée par la CLI.
|
||||||
chiffrée localement.
|
|
||||||
</small>
|
</small>
|
||||||
{isEditing && (
|
|
||||||
<small className="text-xs text-muted">
|
|
||||||
Le profil conserve sa référence de secret existante ; la clé n'est
|
|
||||||
jamais réaffichée côté UI.
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
aria-label={`Enregistrer ${engine}`}
|
|
||||||
loading={saving}
|
|
||||||
disabled={saveDisabled}
|
|
||||||
onClick={() => void save()}
|
|
||||||
className="w-fit"
|
|
||||||
>
|
|
||||||
Enregistrer
|
|
||||||
</Button>
|
|
||||||
</fieldset>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CodexProviderFields({
|
|
||||||
profile,
|
|
||||||
catalog,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
profile: AgentProfile;
|
|
||||||
catalog: CodexProviderCatalog;
|
|
||||||
onChange: (p: AgentProfile) => void;
|
|
||||||
}) {
|
|
||||||
const { profile: profileGateway } = useGateways();
|
|
||||||
return (
|
|
||||||
<ProviderModelSecretFields
|
|
||||||
engine="Codex"
|
|
||||||
profile={profile}
|
|
||||||
catalog={catalog}
|
|
||||||
existing={profile.codexProvider}
|
|
||||||
customSupported={catalog.providers?.some((p) => 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 (
|
|
||||||
<ProviderModelSecretFields
|
|
||||||
engine="Claude"
|
|
||||||
profile={profile}
|
|
||||||
catalog={catalog}
|
|
||||||
existing={profile.claudeProvider}
|
|
||||||
customSupported={false}
|
|
||||||
saveProfile={(input) =>
|
|
||||||
profileGateway.saveClaudeProviderProfile({
|
|
||||||
profile,
|
|
||||||
providerId: input.providerId,
|
|
||||||
model: input.model,
|
|
||||||
apiKey: input.apiKey,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,9 +13,6 @@ import type {
|
|||||||
AgentDrift,
|
AgentDrift,
|
||||||
AgentProfile,
|
AgentProfile,
|
||||||
AppExitWorkGuardState,
|
AppExitWorkGuardState,
|
||||||
ClaudeProviderCatalogEntry,
|
|
||||||
CodexCustomProviderConfig,
|
|
||||||
CodexProviderCatalogEntry,
|
|
||||||
CustomProviderConfig,
|
CustomProviderConfig,
|
||||||
DomainEvent,
|
DomainEvent,
|
||||||
EmbedderEngines,
|
EmbedderEngines,
|
||||||
@ -694,24 +691,6 @@ export interface ProfileGateway {
|
|||||||
saveOpenCodeProviderProfile(
|
saveOpenCodeProviderProfile(
|
||||||
input: SaveOpenCodeProviderProfileInput,
|
input: SaveOpenCodeProviderProfileInput,
|
||||||
): Promise<AgentProfile>;
|
): Promise<AgentProfile>;
|
||||||
/** Static catalogue of Codex providers/models (ticket #99). */
|
|
||||||
listCodexProviders(): Promise<CodexProviderCatalogEntry[]>;
|
|
||||||
/**
|
|
||||||
* 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<AgentProfile>;
|
|
||||||
/** Static catalogue of Claude providers/models (ticket #99). */
|
|
||||||
listClaudeProviders(): Promise<ClaudeProviderCatalogEntry[]>;
|
|
||||||
/**
|
|
||||||
* 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<AgentProfile>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Input for {@link ProfileGateway.cloneOpenCodeProfileFromSeed}. */
|
/** Input for {@link ProfileGateway.cloneOpenCodeProfileFromSeed}. */
|
||||||
@ -740,32 +719,6 @@ export interface SaveOpenCodeProviderProfileInput {
|
|||||||
custom?: CustomProviderConfig;
|
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
|
* Local model servers (F35). CRUD over the global registry of declared
|
||||||
* `llama.cpp` servers an OpenCode profile can bind to via
|
* `llama.cpp` servers an OpenCode profile can bind to via
|
||||||
|
|||||||
Reference in New Issue
Block a user