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:
@ -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")]
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user