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")]

View File

@ -27,8 +27,9 @@ use application::{
InspectConversation, InstallPluginFromArchive, InstallPluginFromDirectory,
JsonPluginManifestValidator, LaunchAgent, LaunchAgentInput, LinkIssues, ListAgents,
ListAgentsInput, ListDevices, ListEmbedderProfiles, ListIssues, ListLayouts, ListMemories,
ListModelServers, ListPluginRuntimeContributions, ListPlugins, ListProfiles, ListProjects,
ListResumableAgents, ListSkills, ListSprints, ListTemplates, LiveAgentRegistry, LiveSessions,
ListModelServers, ListOpenCodeProviders, ListPluginRuntimeContributions, ListPlugins,
ListProfiles, ListProjects, ListResumableAgents, ListSkills, ListSprints, ListTemplates,
LiveAgentRegistry, LiveSessions,
LiveStateLeanProvider, LiveStateProvider, LiveStateReadProvider, LoadLayout, McpRuntime,
McpToolPermissionCatalogue, MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject,
OpenTerminal, OpenTicketAssistant, OrchestratorService, PairAttemptLimiter, PairDevice,
@ -39,7 +40,8 @@ use application::{
RecordTurnProvider, ReferenceProfiles, RenameDevice, RenameLayout, RenameSprint,
ReorderSprints, ResizeTerminal, ResolveAgentPermissions, ResolveMemoryLinks,
RestoreOpenWindows, RetryBackgroundTask, ReviewPluginPackage, RevokeAllDevices, RevokeDevice,
RotateConversationLog, SaveEmbedderProfile, SaveModelServer, SaveProfile, SessionLimitService,
RotateConversationLog, SaveEmbedderProfile, SaveModelServer, SaveOpenCodeProviderProfile,
SaveProfile, SessionLimitService,
SetActiveLayout, SetPluginEnabled, SnapshotOpenWindows, SnapshotRunningAgents,
SpawnBackgroundCommand, StopLiveAgent, StructuredRoutingMode, StructuredSessions,
SuggestedThisSession, SyncAgentWithTemplate, TerminalSessions, TouchDevice,
@ -58,8 +60,8 @@ use domain::ports::{
IssueNumberAllocator, IssueStore, McpToolPermissionStore, MemoryRecall, MemoryStore,
PermissionStore, PluginManifestValidator, PluginMcpSupervisor, PluginPackageStore,
PluginRegistryStore, ProcessSpawner, ProfileStore, ProjectStore, PtyPort, ScheduledTask,
Scheduler, SkillStore, SprintStore, StructuredSessionEnvironmentPreparer, TemplateStore,
ToolInvoker, WakeError, WakeReason, WindowStateStore,
Scheduler, SecretStore, SkillStore, SprintStore, StructuredSessionEnvironmentPreparer,
TemplateStore, ToolInvoker, WakeError, WakeReason, WindowStateStore,
};
use domain::profile::{
AgentProfile, ContextInjection, McpConfigStrategy, McpTransport, StructuredAdapter,
@ -82,7 +84,8 @@ use infrastructure::{
FsIssueNumberAllocator, FsIssueStore, FsLiveStateStore, FsMcpToolPermissionStore,
FsMemoryStore, FsModelServerRegistry, FsOrchestratorWatcher, FsPermissionStore,
FsPluginPackageStore, FsPluginRegistryStore, FsProfileStore, FsProjectStore,
FsProviderSessionStore, FsSkillStore, FsSprintStore, FsTemplateStore, FsWindowStateStore,
FsProviderSessionStore, FsSecretStore, FsSkillStore, FsSprintStore, FsTemplateStore,
FsWindowStateStore,
Git2Repository, HeuristicHandoffSummarizer, HfModelArtifactDownloader,
HttpOpenAiCompatibleProbe, IdeaiContextStore, InMemoryConversationRegistry, InMemoryMailbox,
InMemoryPairAttemptLimiter, LlamaCppRuntime, LocalFileSystem, LocalManagedProcess,
@ -925,6 +928,11 @@ pub struct BackendCore {
pub list_profiles: Arc<ListProfiles>,
/// Save (upsert) a profile.
pub save_profile: Arc<SaveProfile>,
/// Save (upsert) an OpenCode profile backed by a cloud provider, sealing its
/// literal API key into the [`SecretStore`] (ticket #92, lot B3).
pub save_opencode_provider_profile: Arc<SaveOpenCodeProviderProfile>,
/// Static catalogue of OpenCode cloud providers (ticket #92, lot B3).
pub list_opencode_providers: Arc<ListOpenCodeProviders>,
/// Create a new OpenCode profile instance from the canonical seed.
pub clone_opencode_profile_from_seed: Arc<CloneOpenCodeProfileFromSeed>,
/// Delete a profile.
@ -1425,14 +1433,33 @@ impl BackendCore {
let profile_store_port: Arc<dyn ProfileStore> =
Arc::clone(&profile_store) as Arc<dyn ProfileStore>;
// Secret store (ticket #92, lot B2/B3): same app-data dir as `profiles.json`,
// but its own encrypted-at-rest files (`secrets.json` + `secret.key`) so an
// OpenCode cloud provider's API key never lands in plain JSON.
let secret_store = Arc::new(FsSecretStore::new(
Arc::clone(&fs_port),
app_data_dir.to_string_lossy().into_owned(),
));
let secret_store_port: Arc<dyn SecretStore> =
Arc::clone(&secret_store) as Arc<dyn SecretStore>;
let detect_profiles = Arc::new(DetectProfiles::new(Arc::clone(&runtime_port)));
let list_profiles = Arc::new(ListProfiles::new(Arc::clone(&profile_store_port)));
let save_profile = Arc::new(SaveProfile::new(Arc::clone(&profile_store_port)));
let save_opencode_provider_profile = Arc::new(SaveOpenCodeProviderProfile::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 clone_opencode_profile_from_seed = Arc::new(CloneOpenCodeProfileFromSeed::new(
Arc::clone(&profile_store_port),
Arc::clone(&ids) as Arc<dyn IdGenerator>,
));
let delete_profile = Arc::new(DeleteProfile::new(Arc::clone(&profile_store_port)));
let delete_profile = Arc::new(DeleteProfile::new(
Arc::clone(&profile_store_port),
Arc::clone(&secret_store_port),
));
let configure_profiles = Arc::new(ConfigureProfiles::new(Arc::clone(&profile_store_port)));
let reference_profiles = Arc::new(ReferenceProfiles::new());
let first_run_state = Arc::new(FirstRunState::new(Arc::clone(&profile_store_port)));
@ -1587,6 +1614,7 @@ impl BackendCore {
requester: requester.to_owned(),
})
}),
Arc::clone(&secret_store_port),
)) as Arc<dyn StructuredSessionEnvironmentPreparer>;
let open_ticket_assistant = Arc::new(OpenTicketAssistant::new(
Arc::clone(&issue_store_port),
@ -1814,7 +1842,8 @@ impl BackendCore {
.with_live_state_lean(Arc::new(AppLiveStateLeanProvider {
clock: Arc::clone(&clock) as Arc<dyn Clock>,
}) as Arc<dyn LiveStateLeanProvider>)
.with_local_model_server(Arc::clone(&ensure_local_model_server)),
.with_local_model_server(Arc::clone(&ensure_local_model_server))
.with_secret_store(Arc::clone(&secret_store_port)),
);
// Inter-agent launcher: same context, memory, permissions and live-state
@ -1847,6 +1876,7 @@ impl BackendCore {
clock: Arc::clone(&clock) as Arc<dyn Clock>,
}) as Arc<dyn LiveStateLeanProvider>)
.with_local_model_server(Arc::clone(&ensure_local_model_server))
.with_secret_store(Arc::clone(&secret_store_port))
.with_structured(
Arc::clone(&session_factory),
Arc::clone(&structured_sessions),
@ -2587,6 +2617,8 @@ impl BackendCore {
detect_profiles,
list_profiles,
save_profile,
save_opencode_provider_profile,
list_opencode_providers,
clone_opencode_profile_from_seed,
delete_profile,
configure_profiles,