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:
@ -16,8 +16,7 @@ use std::sync::Arc;
|
||||
use domain::ids::ProfileId;
|
||||
use domain::ports::{AgentRuntime, IdGenerator, ProfileStore, SecretRef, SecretStore};
|
||||
use domain::profile::{
|
||||
AgentProfile, ClaudeProviderConfig, CodexCustomProviderConfig, CodexProviderConfig,
|
||||
CustomProviderConfig, OpenCodeConfig, OpenCodeProviderConfig, StructuredAdapter,
|
||||
AgentProfile, CustomProviderConfig, OpenCodeConfig, OpenCodeProviderConfig, StructuredAdapter,
|
||||
};
|
||||
|
||||
use crate::error::AppError;
|
||||
@ -325,149 +324,6 @@ pub struct SaveOpenCodeProviderProfileOutput {
|
||||
pub profile: AgentProfile,
|
||||
}
|
||||
|
||||
/// Input for [`SaveCodexProviderProfile::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SaveCodexProviderProfileInput {
|
||||
/// The profile to create or replace (by id).
|
||||
pub profile: AgentProfile,
|
||||
/// Provider id used as Codex's `model_provider`.
|
||||
pub provider_id: String,
|
||||
/// Model name served by this provider.
|
||||
pub model: String,
|
||||
/// Literal API key, sealed into the `SecretStore`.
|
||||
pub api_key: String,
|
||||
/// Optional custom-provider endpoint configuration.
|
||||
pub custom: Option<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
|
||||
/// B3), keeping the literal API key out of `profiles.json`: it is sealed into the
|
||||
/// [`SecretStore`] under an opaque [`SecretRef`], and only the ref is persisted on
|
||||
@ -580,12 +436,6 @@ impl DeleteProfile {
|
||||
if let Some(config) = &profile.opencode_provider {
|
||||
self.secret_store.delete(&config.api_key_ref).await?;
|
||||
}
|
||||
if let Some(config) = &profile.codex_provider {
|
||||
self.secret_store.delete(&config.api_key_ref).await?;
|
||||
}
|
||||
if let Some(config) = &profile.claude_provider {
|
||||
self.secret_store.delete(&config.api_key_ref).await?;
|
||||
}
|
||||
}
|
||||
self.store.delete(input.id).await?;
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user