feat(acp): replace raw config and secret methods (#9000)

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
This commit is contained in:
Kalvin C
2026-05-04 18:24:28 -07:00
committed by GitHub
parent e82c4fdcb7
commit 2fe4c3d0bb
39 changed files with 1481 additions and 855 deletions
-1
View File
@@ -74,7 +74,6 @@ mod dispatch;
mod extensions;
mod providers;
mod resources;
mod secrets;
mod sessions;
mod sources;
mod tools;
+153 -16
View File
@@ -1,36 +1,173 @@
use super::*;
impl GooseAcpAgent {
pub(super) async fn on_read_config(
pub(super) async fn on_preferences_read(
&self,
req: ReadConfigRequest,
) -> Result<ReadConfigResponse, sacp::Error> {
req: PreferencesReadRequest,
) -> Result<PreferencesReadResponse, sacp::Error> {
let config = self.config()?;
let response = match config.get_param::<serde_json::Value>(&req.key) {
Ok(value) => ReadConfigResponse { value },
Err(crate::config::ConfigError::NotFound(_)) => ReadConfigResponse {
value: serde_json::Value::Null,
},
Err(e) => return Err(sacp::Error::internal_error().data(e.to_string())),
let keys = if req.keys.is_empty() {
PREFERENCE_DEFS.iter().map(|def| def.key).collect()
} else {
req.keys
};
Ok(response)
let mut values = Vec::with_capacity(keys.len());
for key in keys {
let def = preference_def(key)?;
let value = match config.get_param::<serde_json::Value>(def.config_key) {
Ok(value) => value,
Err(crate::config::ConfigError::NotFound(_)) => serde_json::Value::Null,
Err(e) => return Err(sacp::Error::internal_error().data(e.to_string())),
};
values.push(PreferenceValue { key, value });
}
Ok(PreferencesReadResponse { values })
}
pub(super) async fn on_upsert_config(
pub(super) async fn on_preferences_save(
&self,
req: UpsertConfigRequest,
req: PreferencesSaveRequest,
) -> Result<EmptyResponse, sacp::Error> {
let config = self.config()?;
config.set_param(&req.key, &req.value).internal_err()?;
let mut updates = Vec::with_capacity(req.values.len());
for preference in &req.values {
let def = preference_def(preference.key)?;
(def.validate)(&preference.value)?;
updates.push((def.config_key.to_string(), preference.value.clone()));
}
config.set_param_values(&updates).internal_err()?;
Ok(EmptyResponse {})
}
pub(super) async fn on_remove_config(
pub(super) async fn on_preferences_remove(
&self,
req: RemoveConfigRequest,
req: PreferencesRemoveRequest,
) -> Result<EmptyResponse, sacp::Error> {
let config = self.config()?;
config.delete(&req.key).internal_err()?;
for key in req.keys {
let def = preference_def(key)?;
config.delete(def.config_key).internal_err()?;
}
Ok(EmptyResponse {})
}
pub(super) async fn on_defaults_read(
&self,
_req: DefaultsReadRequest,
) -> Result<DefaultsReadResponse, sacp::Error> {
let config = self.config()?;
Ok(DefaultsReadResponse {
provider_id: optional_config_string(&config, "GOOSE_PROVIDER")?,
model_id: optional_config_string(&config, "GOOSE_MODEL")?,
})
}
}
struct PreferenceDef {
key: PreferenceKey,
config_key: &'static str,
validate: fn(&serde_json::Value) -> Result<(), sacp::Error>,
}
const PREFERENCE_DEFS: &[PreferenceDef] = &[
PreferenceDef {
key: PreferenceKey::AutoCompactThreshold,
config_key: "GOOSE_AUTO_COMPACT_THRESHOLD",
validate: validate_auto_compact_threshold,
},
PreferenceDef {
key: PreferenceKey::VoiceAutoSubmitPhrases,
config_key: "VOICE_AUTO_SUBMIT_PHRASES",
validate: validate_voice_auto_submit_phrases,
},
PreferenceDef {
key: PreferenceKey::VoiceDictationProvider,
config_key: "VOICE_DICTATION_PROVIDER",
validate: validate_voice_dictation_provider,
},
PreferenceDef {
key: PreferenceKey::VoiceDictationPreferredMic,
config_key: "VOICE_DICTATION_PREFERRED_MIC",
validate: validate_voice_dictation_preferred_mic,
},
];
fn preference_def(key: PreferenceKey) -> Result<&'static PreferenceDef, sacp::Error> {
PREFERENCE_DEFS
.iter()
.find(|def| def.key == key)
.ok_or_else(|| {
sacp::Error::internal_error().data(format!("Missing preference definition for {key:?}"))
})
}
fn validate_auto_compact_threshold(value: &serde_json::Value) -> Result<(), sacp::Error> {
let Some(value) = value.as_f64() else {
return Err(sacp::Error::invalid_params().data("autoCompactThreshold must be a number"));
};
if !value.is_finite() || value <= 0.0 || value > 1.0 {
return Err(sacp::Error::invalid_params()
.data("autoCompactThreshold must be greater than 0 and at most 1"));
}
Ok(())
}
fn validate_voice_auto_submit_phrases(value: &serde_json::Value) -> Result<(), sacp::Error> {
if !value.is_string() {
return Err(sacp::Error::invalid_params().data("voiceAutoSubmitPhrases must be a string"));
}
Ok(())
}
fn validate_voice_dictation_provider(value: &serde_json::Value) -> Result<(), sacp::Error> {
let Some(value) = value.as_str() else {
return Err(sacp::Error::invalid_params().data("voiceDictationProvider must be a string"));
};
if !is_supported_voice_dictation_provider(value) {
return Err(sacp::Error::invalid_params().data("voiceDictationProvider is not supported"));
}
Ok(())
}
fn validate_voice_dictation_preferred_mic(value: &serde_json::Value) -> Result<(), sacp::Error> {
let Some(value) = value.as_str() else {
return Err(
sacp::Error::invalid_params().data("voiceDictationPreferredMic must be a string")
);
};
if value.is_empty() {
return Err(
sacp::Error::invalid_params().data("voiceDictationPreferredMic must be non-empty")
);
}
Ok(())
}
fn is_supported_voice_dictation_provider(value: &str) -> bool {
matches!(value, "openai" | "groq" | "elevenlabs" | "__disabled__") || {
#[cfg(feature = "local-inference")]
{
value == "local"
}
#[cfg(not(feature = "local-inference"))]
{
false
}
}
}
fn optional_config_string(config: &Config, key: &str) -> Result<Option<String>, sacp::Error> {
match config.get_param::<String>(key) {
Ok(value) => Ok(Some(value)),
Err(crate::config::ConfigError::NotFound(_)) => Ok(None),
Err(e) => Err(sacp::Error::internal_error().data(e.to_string())),
}
}
+34 -34
View File
@@ -200,52 +200,36 @@ impl GooseAcpAgent {
self.on_delete_provider_config(req).await
}
#[custom_method(ReadConfigRequest)]
async fn dispatch_read_config(
#[custom_method(PreferencesReadRequest)]
async fn dispatch_preferences_read(
&self,
req: ReadConfigRequest,
) -> Result<ReadConfigResponse, sacp::Error> {
self.on_read_config(req).await
req: PreferencesReadRequest,
) -> Result<PreferencesReadResponse, sacp::Error> {
self.on_preferences_read(req).await
}
#[custom_method(UpsertConfigRequest)]
async fn dispatch_upsert_config(
#[custom_method(PreferencesSaveRequest)]
async fn dispatch_preferences_save(
&self,
req: UpsertConfigRequest,
req: PreferencesSaveRequest,
) -> Result<EmptyResponse, sacp::Error> {
self.on_upsert_config(req).await
self.on_preferences_save(req).await
}
#[custom_method(RemoveConfigRequest)]
async fn dispatch_remove_config(
#[custom_method(PreferencesRemoveRequest)]
async fn dispatch_preferences_remove(
&self,
req: RemoveConfigRequest,
req: PreferencesRemoveRequest,
) -> Result<EmptyResponse, sacp::Error> {
self.on_remove_config(req).await
self.on_preferences_remove(req).await
}
#[custom_method(CheckSecretRequest)]
async fn dispatch_check_secret(
#[custom_method(DefaultsReadRequest)]
async fn dispatch_defaults_read(
&self,
req: CheckSecretRequest,
) -> Result<CheckSecretResponse, sacp::Error> {
self.on_check_secret(req).await
}
#[custom_method(UpsertSecretRequest)]
async fn dispatch_upsert_secret(
&self,
req: UpsertSecretRequest,
) -> Result<EmptyResponse, sacp::Error> {
self.on_upsert_secret(req).await
}
#[custom_method(RemoveSecretRequest)]
async fn dispatch_remove_secret(
&self,
req: RemoveSecretRequest,
) -> Result<EmptyResponse, sacp::Error> {
self.on_remove_secret(req).await
req: DefaultsReadRequest,
) -> Result<DefaultsReadResponse, sacp::Error> {
self.on_defaults_read(req).await
}
#[custom_method(ExportSessionRequest)]
@@ -360,6 +344,22 @@ impl GooseAcpAgent {
self.on_dictation_config(_req).await
}
#[custom_method(DictationSecretSaveRequest)]
async fn dispatch_dictation_secret_save(
&self,
req: DictationSecretSaveRequest,
) -> Result<EmptyResponse, sacp::Error> {
self.on_dictation_secret_save(req).await
}
#[custom_method(DictationSecretDeleteRequest)]
async fn dispatch_dictation_secret_delete(
&self,
req: DictationSecretDeleteRequest,
) -> Result<EmptyResponse, sacp::Error> {
self.on_dictation_secret_delete(req).await
}
#[custom_method(DictationModelsListRequest)]
async fn dispatch_dictation_models_list(
&self,
+48 -1
View File
@@ -2,7 +2,7 @@ use super::*;
#[cfg(feature = "local-inference")]
use crate::dictation::providers::transcribe_local;
use crate::dictation::providers::{
all_providers, is_configured, transcribe_with_provider, DictationProvider,
all_providers, get_provider_def, is_configured, transcribe_with_provider, DictationProvider,
};
#[cfg(feature = "local-inference")]
use crate::dictation::whisper;
@@ -125,6 +125,30 @@ impl GooseAcpAgent {
Ok(DictationConfigResponse { providers })
}
pub(super) async fn on_dictation_secret_save(
&self,
req: DictationSecretSaveRequest,
) -> Result<EmptyResponse, sacp::Error> {
let provider = parse_dictation_provider(&req.provider)?;
let key = dictation_secret_config_key(provider)?;
let config = self.config()?;
config.set_secret(key, &req.value).internal_err()?;
Config::global().invalidate_secrets_cache();
Ok(EmptyResponse {})
}
pub(super) async fn on_dictation_secret_delete(
&self,
req: DictationSecretDeleteRequest,
) -> Result<EmptyResponse, sacp::Error> {
let provider = parse_dictation_provider(&req.provider)?;
let key = dictation_secret_config_key(provider)?;
let config = self.config()?;
config.delete_secret(key).internal_err()?;
Config::global().invalidate_secrets_cache();
Ok(EmptyResponse {})
}
pub(super) async fn on_dictation_models_list(
&self,
_req: DictationModelsListRequest,
@@ -321,6 +345,29 @@ impl GooseAcpAgent {
Ok(EmptyResponse {})
}
}
fn parse_dictation_provider(provider: &str) -> Result<DictationProvider, sacp::Error> {
serde_json::from_value(serde_json::Value::String(provider.to_string()))
.map_err(|_| sacp::Error::invalid_params().data(format!("Unknown provider: {provider}")))
}
fn dictation_secret_config_key(provider: DictationProvider) -> Result<&'static str, sacp::Error> {
let def = get_provider_def(provider);
if def.uses_provider_config {
return Err(sacp::Error::invalid_params().data(
"Dictation provider uses the main provider configuration. Configure its credentials in provider settings instead.",
));
}
#[cfg(feature = "local-inference")]
if provider == DictationProvider::Local {
return Err(sacp::Error::invalid_params()
.data("Dictation provider does not use an API key or secret."));
}
Ok(def.config_key)
}
fn dictation_model_config_key(provider: DictationProvider) -> Option<String> {
match provider {
DictationProvider::OpenAI => Some(OPENAI_TRANSCRIPTION_MODEL_CONFIG_KEY.to_string()),
-32
View File
@@ -1,32 +0,0 @@
use super::*;
impl GooseAcpAgent {
pub(super) async fn on_check_secret(
&self,
req: CheckSecretRequest,
) -> Result<CheckSecretResponse, sacp::Error> {
let config = self.config()?;
let exists = config.get_secret::<serde_json::Value>(&req.key).is_ok();
Ok(CheckSecretResponse { exists })
}
pub(super) async fn on_upsert_secret(
&self,
req: UpsertSecretRequest,
) -> Result<EmptyResponse, sacp::Error> {
let config = self.config()?;
config.set_secret(&req.key, &req.value).internal_err()?;
Config::global().invalidate_secrets_cache();
Ok(EmptyResponse {})
}
pub(super) async fn on_remove_secret(
&self,
req: RemoveSecretRequest,
) -> Result<EmptyResponse, sacp::Error> {
let config = self.config()?;
config.delete_secret(&req.key).internal_err()?;
Config::global().invalidate_secrets_cache();
Ok(EmptyResponse {})
}
}
+14
View File
@@ -719,6 +719,20 @@ impl Config {
self.save_values(&values)
}
/// Set multiple configuration values in the config file with one read and one write.
pub fn set_param_values(&self, updates: &[(String, Value)]) -> Result<(), ConfigError> {
if updates.is_empty() {
return Ok(());
}
let _guard = self.guard.lock().unwrap();
let mut values = self.load_write_config()?;
for (key, value) in updates {
values.insert(serde_yaml::to_value(key)?, serde_yaml::to_value(value)?);
}
self.save_values(&values)
}
/// Delete a configuration value in the config file.
///
/// This will immediately write the value to the config file. The value