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:
@ -17,10 +17,10 @@ use std::sync::Arc;
|
||||
use domain::ports::{
|
||||
AgentContextStore, AgentRuntime, AgentSessionFactory, ContextInjectionPlan, EventBus,
|
||||
FileSystem, FsError, IdGenerator, MemoryQuery, MemoryRecall, PermissionStore, PreparedContext,
|
||||
ProfileStore, ProjectStore, PtyPort, RemotePath, SessionPlan, SkillStore, SpawnSpec,
|
||||
StoreError,
|
||||
ProfileStore, ProjectStore, PtyPort, RemotePath, SecretStore, SessionPlan, SkillStore,
|
||||
SpawnSpec, StoreError,
|
||||
};
|
||||
use domain::profile::{McpConfigStrategy, StructuredAdapter};
|
||||
use domain::profile::{McpConfigStrategy, OpenCodeProviderConfig, StructuredAdapter};
|
||||
use domain::sandbox::{compile_sandbox_plan, SandboxContext, SandboxPlan};
|
||||
use domain::{
|
||||
bound_handoff_summary, Agent, AgentId, AgentManifest, AgentOrigin, AgentProfile,
|
||||
@ -1150,6 +1150,12 @@ pub struct LaunchAgent {
|
||||
/// `opencode.json` (B35). Optional for legacy tests/wiring; required at runtime
|
||||
/// when `OpenCodeConfig.localModelServerId` is set.
|
||||
local_model_server: Option<Arc<EnsureLocalModelServer>>,
|
||||
/// Resolves the literal API key of an [`domain::profile::OpenCodeProviderConfig`]
|
||||
/// (ticket #92, lot B3) just before writing `opencode.json`. Optional for legacy
|
||||
/// tests/wiring; required at runtime when `profile.opencode_provider` is set — a
|
||||
/// missing store or a missing/undecryptable secret **fails the launch**
|
||||
/// (never spawns with a dead/absent key).
|
||||
secret_store: Option<Arc<dyn SecretStore>>,
|
||||
/// Explicit intent for structured profiles when structured ports are absent.
|
||||
structured_routing_mode: StructuredRoutingMode,
|
||||
}
|
||||
@ -1191,6 +1197,7 @@ impl LaunchAgent {
|
||||
projectors: None,
|
||||
live_state_lean: None,
|
||||
local_model_server: None,
|
||||
secret_store: None,
|
||||
structured_routing_mode: StructuredRoutingMode::HumanPtyFallback,
|
||||
}
|
||||
}
|
||||
@ -1209,6 +1216,17 @@ impl LaunchAgent {
|
||||
self
|
||||
}
|
||||
|
||||
/// Injects the [`SecretStore`] used to resolve an
|
||||
/// [`domain::profile::OpenCodeProviderConfig`]'s literal API key at launch
|
||||
/// (ticket #92, lot B3). Without this call (legacy call sites / tests), a
|
||||
/// profile carrying `opencode_provider` fails its launch instead of silently
|
||||
/// writing an unauthenticated `opencode.json`.
|
||||
#[must_use]
|
||||
pub fn with_secret_store(mut self, secret_store: Arc<dyn SecretStore>) -> Self {
|
||||
self.secret_store = Some(secret_store);
|
||||
self
|
||||
}
|
||||
|
||||
/// Branche le provider de **live-state lean (lot LS4)** : au lancement, l'aperçu
|
||||
/// `# État du projet` (status + intent des autres agents) est injecté dans le
|
||||
/// convention file. Sans cet appel (cas legacy / tests existants), aucune section
|
||||
@ -1697,7 +1715,7 @@ impl LaunchAgent {
|
||||
input.mcp_runtime.as_ref(),
|
||||
&mut spec,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
|
||||
// 5c. ── PROJECTION DES PERMISSIONS (lot LP3-3) ──
|
||||
// Strictement APRÈS le convention file (5) ET la conf MCP (5a), donc
|
||||
@ -2254,9 +2272,9 @@ impl LaunchAgent {
|
||||
project_root: &ProjectPath,
|
||||
runtime: Option<&McpRuntime>,
|
||||
spec: &mut SpawnSpec,
|
||||
) {
|
||||
) -> Result<(), AppError> {
|
||||
let Some(mcp) = &profile.mcp else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
match &mcp.config {
|
||||
domain::profile::McpConfigStrategy::ConfigFile { target } => {
|
||||
@ -2342,10 +2360,21 @@ impl LaunchAgent {
|
||||
}
|
||||
domain::profile::McpConfigStrategy::OpenCodeConfig { target } => {
|
||||
if profile.structured_adapter != Some(StructuredAdapter::OpenCode) {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
let Some(opencode) = profile.opencode.as_ref() else {
|
||||
return;
|
||||
let body = if let Some(opencode) = profile.opencode.as_ref() {
|
||||
opencode_config_json(opencode, project_root.as_str(), runtime).to_string()
|
||||
} else if let Some(provider) = profile.opencode_provider.as_ref() {
|
||||
let api_key = self.resolve_opencode_provider_api_key(provider).await?;
|
||||
opencode_provider_config_json(
|
||||
provider,
|
||||
&api_key,
|
||||
project_root.as_str(),
|
||||
runtime,
|
||||
)
|
||||
.to_string()
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
let config_path = join(run_dir, target);
|
||||
let opencode_home = join(run_dir, ".opencode");
|
||||
@ -2361,8 +2390,6 @@ impl LaunchAgent {
|
||||
.create_dir_all(&RemotePath::new(format!("{}/{parent}", run_dir.as_str())))
|
||||
.await;
|
||||
}
|
||||
let body =
|
||||
opencode_config_json(opencode, project_root.as_str(), runtime).to_string();
|
||||
let _ = self
|
||||
.fs
|
||||
.write(&RemotePath::new(config_path.clone()), body.as_bytes())
|
||||
@ -2387,6 +2414,39 @@ impl LaunchAgent {
|
||||
spec.env.push((var.clone(), run_dir.as_str().to_owned()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolves the literal API key of an [`OpenCodeProviderConfig`] through the
|
||||
/// injected [`SecretStore`] (ticket #92, lot B3). Unlike the rest of
|
||||
/// [`Self::apply_mcp_config`] (best-effort, never fails a launch), this fails
|
||||
/// **hard**: an OpenCode cloud profile with no resolvable key must never spawn
|
||||
/// with a dead/absent key, silently falling back to the CLI's native provider
|
||||
/// prompting.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Invalid`] if no [`SecretStore`] was injected
|
||||
/// ([`Self::with_secret_store`]) or the secret is absent (corrupted/deleted
|
||||
/// out from under the profile); [`AppError::Store`] on store failure.
|
||||
async fn resolve_opencode_provider_api_key(
|
||||
&self,
|
||||
provider: &OpenCodeProviderConfig,
|
||||
) -> Result<String, AppError> {
|
||||
let secret_store = self.secret_store.as_ref().ok_or_else(|| {
|
||||
AppError::Invalid(
|
||||
"OpenCode cloud 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 OpenCode provider `{}` (secret ref `{}`)",
|
||||
provider.provider_id,
|
||||
provider.api_key_ref.as_str()
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
async fn ensure_local_model_server_for_opencode(
|
||||
@ -2679,6 +2739,66 @@ fn opencode_config_json(
|
||||
serde_json::Value::Object(root)
|
||||
}
|
||||
|
||||
/// Renders the OpenCode config for a **cloud** provider profile (ticket #92, lot
|
||||
/// B3), mirroring [`opencode_config_json`]. Unlike the `llamacpp` custom-provider
|
||||
/// shape, `provider.provider_id` is a BUILT-IN OpenCode provider (`anthropic`,
|
||||
/// `openrouter`, …): it needs only the resolved `apiKey` override, no
|
||||
/// `npm`/`name`/`models` block, and it must NOT appear in `disabled_providers`
|
||||
/// (that list exists to keep the local llamacpp profile from picking up a
|
||||
/// built-in provider by accident — the opposite of what a cloud profile wants).
|
||||
fn opencode_provider_config_json(
|
||||
config: &OpenCodeProviderConfig,
|
||||
api_key: &str,
|
||||
project_root: &str,
|
||||
runtime: Option<&McpRuntime>,
|
||||
) -> serde_json::Value {
|
||||
let mut root = serde_json::Map::new();
|
||||
root.insert(
|
||||
"$schema".to_owned(),
|
||||
serde_json::Value::String("https://opencode.ai/config.json".to_owned()),
|
||||
);
|
||||
root.insert(
|
||||
"model".to_owned(),
|
||||
serde_json::Value::String(format!("{}/{}", config.provider_id, config.model)),
|
||||
);
|
||||
root.insert(
|
||||
"provider".to_owned(),
|
||||
serde_json::json!({
|
||||
config.provider_id.as_str(): {
|
||||
"options": {
|
||||
"apiKey": api_key
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let wiring = mcp_server_wiring(domain::profile::McpTransport::Stdio, runtime);
|
||||
let command_array = std::iter::once(wiring.command)
|
||||
.chain(wiring.args)
|
||||
.map(serde_json::Value::String)
|
||||
.collect::<Vec<_>>();
|
||||
root.insert(
|
||||
"mcp".to_owned(),
|
||||
serde_json::json!({
|
||||
"idea": {
|
||||
"type": "local",
|
||||
"command": command_array,
|
||||
"cwd": project_root,
|
||||
"enabled": true,
|
||||
"timeout": 15000
|
||||
}
|
||||
}),
|
||||
);
|
||||
root.insert(
|
||||
"permission".to_owned(),
|
||||
serde_json::json!({
|
||||
"bash": "ask",
|
||||
"edit": "ask"
|
||||
}),
|
||||
);
|
||||
serde_json::Value::Object(root)
|
||||
}
|
||||
|
||||
/// Builds an absolute path string by joining a [`ProjectPath`] with a relative
|
||||
/// segment using a POSIX separator.
|
||||
fn join(base: &ProjectPath, rel: &str) -> String {
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
mod catalogue;
|
||||
mod inspect;
|
||||
mod lifecycle;
|
||||
mod provider_catalogue;
|
||||
mod resume;
|
||||
mod session_limit;
|
||||
mod structured;
|
||||
@ -28,6 +29,10 @@ pub use catalogue::{
|
||||
reference_profile_id, reference_profiles, selectable_reference_profiles, CODEX_SUBMIT_DELAY_MS,
|
||||
};
|
||||
pub use inspect::{InspectConversation, InspectConversationInput, InspectConversationOutput};
|
||||
pub use provider_catalogue::{
|
||||
opencode_provider_catalogue, ListOpenCodeProviders, ListOpenCodeProvidersOutput,
|
||||
OpenCodeProviderCatalogEntry,
|
||||
};
|
||||
pub use lifecycle::{
|
||||
ChangeAgentProfile, ChangeAgentProfileInput, ChangeAgentProfileOutput, CreateAgentFromScratch,
|
||||
CreateAgentInput, CreateAgentOutput, DeleteAgent, DeleteAgentInput, HandoffProvider,
|
||||
@ -46,5 +51,6 @@ pub use usecases::{
|
||||
ConfigureProfilesOutput, DeleteProfile, DeleteProfileInput, DetectProfiles,
|
||||
DetectProfilesInput, DetectProfilesOutput, FirstRunState, FirstRunStateOutput, ListProfiles,
|
||||
ListProfilesOutput, ProfileAvailability, ReferenceProfiles, ReferenceProfilesOutput,
|
||||
SaveProfile, SaveProfileInput, SaveProfileOutput,
|
||||
SaveOpenCodeProviderProfile, SaveOpenCodeProviderProfileInput,
|
||||
SaveOpenCodeProviderProfileOutput, SaveProfile, SaveProfileInput, SaveProfileOutput,
|
||||
};
|
||||
|
||||
110
crates/application/src/agent/provider_catalogue.rs
Normal file
110
crates/application/src/agent/provider_catalogue.rs
Normal file
@ -0,0 +1,110 @@
|
||||
//! Static catalogue of OpenCode **cloud** providers (ticket #92, lot B3).
|
||||
//!
|
||||
//! No OpenCode sub-command exposes a stable, machine-readable provider list
|
||||
//! (cadrage Architect), so the catalogue is hard-coded data — same pattern as
|
||||
//! [`super::catalogue::reference_profiles`]: a product decision about *which*
|
||||
//! providers to offer, expressed as data, not code (Open/Closed).
|
||||
|
||||
/// One entry of the OpenCode cloud-provider catalogue.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct OpenCodeProviderCatalogEntry {
|
||||
/// Identifier in the OpenCode provider registry (e.g. `"anthropic"`).
|
||||
pub provider_id: &'static str,
|
||||
/// Human-readable label for the picker UI.
|
||||
pub display_name: &'static str,
|
||||
/// Model names this provider serves, offered for selection.
|
||||
pub models: &'static [&'static str],
|
||||
}
|
||||
|
||||
/// Returns the static OpenCode cloud-provider catalogue.
|
||||
#[must_use]
|
||||
pub fn opencode_provider_catalogue() -> &'static [OpenCodeProviderCatalogEntry] {
|
||||
&[
|
||||
OpenCodeProviderCatalogEntry {
|
||||
provider_id: "anthropic",
|
||||
display_name: "Anthropic",
|
||||
models: &[
|
||||
"claude-sonnet-5",
|
||||
"claude-opus-4-8",
|
||||
"claude-haiku-4-5-20251001",
|
||||
],
|
||||
},
|
||||
OpenCodeProviderCatalogEntry {
|
||||
provider_id: "openrouter",
|
||||
display_name: "OpenRouter",
|
||||
models: &[
|
||||
"anthropic/claude-sonnet-5",
|
||||
"openai/gpt-5",
|
||||
"google/gemini-3-pro",
|
||||
],
|
||||
},
|
||||
OpenCodeProviderCatalogEntry {
|
||||
provider_id: "openai",
|
||||
display_name: "OpenAI",
|
||||
models: &["gpt-5", "gpt-5-mini"],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// Use case exposing [`opencode_provider_catalogue`] to the driving side. No
|
||||
/// port: the catalogue is pure static data, not something to fetch through I/O.
|
||||
pub struct ListOpenCodeProviders;
|
||||
|
||||
/// Output of [`ListOpenCodeProviders::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ListOpenCodeProvidersOutput {
|
||||
/// The static catalogue entries.
|
||||
pub providers: Vec<OpenCodeProviderCatalogEntry>,
|
||||
}
|
||||
|
||||
impl ListOpenCodeProviders {
|
||||
/// Builds the use case (stateless, no ports to inject).
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// Lists the static OpenCode cloud-provider catalogue.
|
||||
#[must_use]
|
||||
pub fn execute(&self) -> ListOpenCodeProvidersOutput {
|
||||
ListOpenCodeProvidersOutput {
|
||||
providers: opencode_provider_catalogue().to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ListOpenCodeProviders {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn catalogue_entries_have_non_empty_ids_and_models() {
|
||||
for entry in opencode_provider_catalogue() {
|
||||
assert!(!entry.provider_id.is_empty());
|
||||
assert!(!entry.display_name.is_empty());
|
||||
assert!(!entry.models.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_ids_are_unique() {
|
||||
let catalogue = opencode_provider_catalogue();
|
||||
for (i, a) in catalogue.iter().enumerate() {
|
||||
for b in &catalogue[i + 1..] {
|
||||
assert_ne!(a.provider_id, b.provider_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_opencode_providers_returns_the_static_catalogue() {
|
||||
let output = ListOpenCodeProviders::new().execute();
|
||||
assert_eq!(output.providers.len(), opencode_provider_catalogue().len());
|
||||
}
|
||||
}
|
||||
@ -14,8 +14,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ids::ProfileId;
|
||||
use domain::ports::{AgentRuntime, IdGenerator, ProfileStore};
|
||||
use domain::profile::{AgentProfile, OpenCodeConfig, StructuredAdapter};
|
||||
use domain::ports::{AgentRuntime, IdGenerator, ProfileStore, SecretRef, SecretStore};
|
||||
use domain::profile::{AgentProfile, OpenCodeConfig, OpenCodeProviderConfig, StructuredAdapter};
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
@ -272,6 +272,95 @@ impl SaveProfile {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SaveOpenCodeProviderProfile
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`SaveOpenCodeProviderProfile::execute`]: the profile to upsert
|
||||
/// (with [`AgentProfile::opencode_provider`] left as-is — this use case fills it
|
||||
/// in) plus the **literal** provider fields. The literal `api_key` never reaches
|
||||
/// [`ProfileStore`] — only [`SaveOpenCodeProviderProfile`] is allowed to touch it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SaveOpenCodeProviderProfileInput {
|
||||
/// The profile to create or replace (by id). Its `opencode_provider` field is
|
||||
/// overwritten by this use case; any value set on it is ignored.
|
||||
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. Minted into a fresh [`SecretRef`] on first save, or
|
||||
/// re-sealed under the profile's existing `SecretRef` on edit — never
|
||||
/// persisted as a literal in `profiles.json`.
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
/// Output of [`SaveOpenCodeProviderProfile::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SaveOpenCodeProviderProfileOutput {
|
||||
/// The saved profile (echoed back), with `opencode_provider` set.
|
||||
pub profile: AgentProfile,
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// [`domain::profile::OpenCodeProviderConfig`].
|
||||
pub struct SaveOpenCodeProviderProfile {
|
||||
profile_store: Arc<dyn ProfileStore>,
|
||||
secret_store: Arc<dyn SecretStore>,
|
||||
ids: Arc<dyn IdGenerator>,
|
||||
}
|
||||
|
||||
impl SaveOpenCodeProviderProfile {
|
||||
/// 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`] (minted fresh, or reused from
|
||||
/// the profile's existing `opencode_provider` when editing) and persists the
|
||||
/// profile with `opencode_provider` set to the resulting
|
||||
/// [`domain::profile::OpenCodeProviderConfig`].
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Invalid`] if `provider_id`/`model` is empty, [`AppError::Store`]
|
||||
/// on secret or profile persistence failure.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: SaveOpenCodeProviderProfileInput,
|
||||
) -> Result<SaveOpenCodeProviderProfileOutput, AppError> {
|
||||
let secret_ref = input
|
||||
.profile
|
||||
.opencode_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 =
|
||||
OpenCodeProviderConfig::new(input.provider_id, input.model, secret_ref)
|
||||
.map_err(|e| AppError::Invalid(e.to_string()))?;
|
||||
|
||||
let mut profile = input.profile;
|
||||
profile.opencode_provider = Some(provider);
|
||||
|
||||
self.profile_store.save(&profile).await?;
|
||||
Ok(SaveOpenCodeProviderProfileOutput { profile })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DeleteProfile
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -283,24 +372,37 @@ pub struct DeleteProfileInput {
|
||||
pub id: domain::ids::ProfileId,
|
||||
}
|
||||
|
||||
/// Deletes a profile by id.
|
||||
/// Deletes a profile by id. If the profile carries an
|
||||
/// [`domain::profile::OpenCodeProviderConfig`], its secret is removed from the
|
||||
/// [`SecretStore`] first, so no orphaned secret is left behind (ticket #92, lot
|
||||
/// B3).
|
||||
pub struct DeleteProfile {
|
||||
store: Arc<dyn ProfileStore>,
|
||||
secret_store: Arc<dyn SecretStore>,
|
||||
}
|
||||
|
||||
impl DeleteProfile {
|
||||
/// Builds the use case from the [`ProfileStore`] port.
|
||||
/// Builds the use case from the [`ProfileStore`] and [`SecretStore`] ports.
|
||||
#[must_use]
|
||||
pub fn new(store: Arc<dyn ProfileStore>) -> Self {
|
||||
Self { store }
|
||||
pub fn new(store: Arc<dyn ProfileStore>, secret_store: Arc<dyn SecretStore>) -> Self {
|
||||
Self {
|
||||
store,
|
||||
secret_store,
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes the profile.
|
||||
/// Deletes the profile (and its secret, if any).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::NotFound`] if the id is unknown, [`AppError::Store`] on
|
||||
/// persistence failure.
|
||||
pub async fn execute(&self, input: DeleteProfileInput) -> Result<(), AppError> {
|
||||
let profiles = self.store.list().await?;
|
||||
if let Some(profile) = profiles.into_iter().find(|p| p.id == input.id) {
|
||||
if let Some(config) = &profile.opencode_provider {
|
||||
self.secret_store.delete(&config.api_key_ref).await?;
|
||||
}
|
||||
}
|
||||
self.store.delete(input.id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
use domain::ports::{
|
||||
AgentSessionError, EmbedderError, FsError, GitError, MemoryError, ModelServerError,
|
||||
ProcessError, PtyError, RemoteError, RuntimeError, StoreError,
|
||||
ProcessError, PtyError, RemoteError, RuntimeError, SecretStoreError, StoreError,
|
||||
};
|
||||
use domain::{AgentId, NodeId};
|
||||
use domain::{IssueStoreError, SprintStoreError};
|
||||
@ -236,6 +236,14 @@ impl From<RemoteError> for AppError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SecretStoreError> for AppError {
|
||||
/// Maps to [`AppError::Store`] — a [`domain::ports::SecretStore`] failure is a
|
||||
/// persistence failure like any other store, coherent with [`StoreError`].
|
||||
fn from(e: SecretStoreError) -> Self {
|
||||
Self::Store(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AgentSessionError> for AppError {
|
||||
/// Maps a structured [`AgentSessionError`] (ARCHITECTURE §17.1) onto the single
|
||||
/// application error shape. `Start`/`Io`/`Decode`/`Timeout` are all execution
|
||||
|
||||
@ -50,14 +50,16 @@ pub use agent::{
|
||||
DetectProfilesInput, DetectProfilesOutput, FirstRunState, FirstRunStateOutput, HandoffProvider,
|
||||
InjectedLiveRow, InspectConversation, InspectConversationInput, InspectConversationOutput,
|
||||
LaunchAgent, LaunchAgentInput, LaunchAgentOutput, ListAgents, ListAgentsInput,
|
||||
ListAgentsOutput, ListProfiles, ListProfilesOutput, ListResumableAgents,
|
||||
ListResumableAgentsInput, ListResumableAgentsOutput, LiveStateLeanProvider, McpRuntime,
|
||||
PermissionProjectorRegistry, ProfileAvailability, ProviderSessionProvider, ReadAgentContext,
|
||||
ReadAgentContextInput, ReadAgentContextOutput, ReferenceProfiles, ReferenceProfilesOutput,
|
||||
ResumableAgent, SaveProfile, SaveProfileInput, SaveProfileOutput, SessionLimitService,
|
||||
StructuredRoutingMode, StructuredSessionDescriptor, TurnOutcome, UpdateAgentContext,
|
||||
UpdateAgentContextInput, AGENT_MEMORY_RECALL_BUDGET, CODEX_SUBMIT_DELAY_MS,
|
||||
LIVE_STATE_INJECT_MAX, RESUME_PROMPT,
|
||||
ListAgentsOutput, ListOpenCodeProviders, ListOpenCodeProvidersOutput, ListProfiles,
|
||||
ListProfilesOutput, ListResumableAgents, ListResumableAgentsInput, ListResumableAgentsOutput,
|
||||
LiveStateLeanProvider, McpRuntime, OpenCodeProviderCatalogEntry, PermissionProjectorRegistry,
|
||||
ProfileAvailability, ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput,
|
||||
ReadAgentContextOutput, ReferenceProfiles, ReferenceProfilesOutput, ResumableAgent,
|
||||
SaveOpenCodeProviderProfile, SaveOpenCodeProviderProfileInput,
|
||||
SaveOpenCodeProviderProfileOutput, SaveProfile, 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::{
|
||||
BackgroundCommandArchive, CancelBackgroundTask, CancelBackgroundTaskOutput,
|
||||
|
||||
Reference in New Issue
Block a user