feat(backend): support des providers OpenCode cloud (#92)

Ajoute le catalogue statique de providers OpenCode (lot B3), le stockage
sécurisé des secrets (SecretStore + adapter infrastructure), et les
use cases SaveOpenCodeProviderProfile/DeleteProfile câblés en composition
root. Couvre le fix B1 et les tests de régression demandés par QA.

cargo build --workspace propre, cargo test --workspace -- --test-threads=1
intégralement vert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 08:03:03 +02:00
parent bece7c92c5
commit 23a3c2788f
20 changed files with 1311 additions and 55 deletions

View File

@ -996,7 +996,8 @@ use application::{
CloneOpenCodeProfileFromSeedInput, CloneOpenCodeProfileFromSeedOutput, ConfigureProfilesInput,
ConfigureProfilesOutput, DeleteProfileInput, DetectProfilesInput, DetectProfilesOutput,
FirstRunStateOutput, ListProfilesOutput, ProfileAvailability, ReferenceProfilesOutput,
SaveProfileInput, SaveProfileOutput,
SaveOpenCodeProviderProfileInput, SaveOpenCodeProviderProfileOutput, SaveProfileInput,
SaveProfileOutput,
};
use domain::profile::{AgentProfile, OpenCodeConfig};
use domain::ProfileId;
@ -1043,6 +1044,40 @@ impl From<CloneOpenCodeProfileFromSeedOutput> for ProfileDto {
}
}
/// One entry of the static OpenCode cloud-provider catalogue (ticket #92, lot B3).
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenCodeProviderDto {
/// Identifier in the OpenCode provider registry (e.g. `"anthropic"`).
pub provider_id: String,
/// Human-readable label for the picker UI.
pub display_name: String,
/// Model names this provider serves, offered for selection.
pub models: Vec<String>,
}
impl From<application::OpenCodeProviderCatalogEntry> for OpenCodeProviderDto {
fn from(entry: application::OpenCodeProviderCatalogEntry) -> Self {
Self {
provider_id: entry.provider_id.to_owned(),
display_name: entry.display_name.to_owned(),
models: entry.models.iter().map(|&m| m.to_owned()).collect(),
}
}
}
/// A list of OpenCode cloud-provider catalogue entries (camelCase array on the
/// wire).
#[derive(Debug, Clone, Serialize)]
#[serde(transparent)]
pub struct OpenCodeProviderListDto(pub Vec<OpenCodeProviderDto>);
impl From<application::ListOpenCodeProvidersOutput> for OpenCodeProviderListDto {
fn from(out: application::ListOpenCodeProvidersOutput) -> Self {
Self(out.providers.into_iter().map(Into::into).collect())
}
}
/// Request DTO for `detect_profiles`: the candidate profiles to probe.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -1105,6 +1140,39 @@ impl From<SaveProfileRequestDto> for SaveProfileInput {
}
}
/// Request DTO for `save_opencode_provider_profile` (ticket #92, lot B3): the
/// profile to upsert plus the literal provider fields. `apiKey` never reaches
/// `profiles.json` — the use case seals it into the `SecretStore`.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SaveOpenCodeProviderProfileRequestDto {
/// The profile to create or replace (by id).
pub profile: AgentProfile,
/// Provider id in the OpenCode registry (e.g. `"anthropic"`).
pub provider_id: String,
/// Model name served by this provider.
pub model: String,
/// Literal API key, sealed into the `SecretStore` — never persisted as-is.
pub api_key: String,
}
impl From<SaveOpenCodeProviderProfileRequestDto> for SaveOpenCodeProviderProfileInput {
fn from(dto: SaveOpenCodeProviderProfileRequestDto) -> Self {
Self {
profile: dto.profile,
provider_id: dto.provider_id,
model: dto.model,
api_key: dto.api_key,
}
}
}
impl From<SaveOpenCodeProviderProfileOutput> for ProfileDto {
fn from(out: SaveOpenCodeProviderProfileOutput) -> Self {
Self(out.profile)
}
}
/// Request DTO for `clone_opencode_profile_from_seed`.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]