feat(acp): replace raw config and secret methods (#9000)
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
This commit is contained in:
@@ -173,69 +173,85 @@ pub struct GetSessionExtensionsResponse {
|
||||
pub extensions: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Read a single non-secret config value.
|
||||
/// Read allowlisted user preferences. Empty `keys` means all supported preferences.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/config/read", response = ReadConfigResponse)]
|
||||
#[request(method = "_goose/preferences/read", response = PreferencesReadResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReadConfigRequest {
|
||||
pub key: String,
|
||||
pub struct PreferencesReadRequest {
|
||||
#[serde(default)]
|
||||
pub keys: Vec<PreferenceKey>,
|
||||
}
|
||||
|
||||
/// Config read response.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
|
||||
/// Save allowlisted user preferences.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/preferences/save", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReadConfigResponse {
|
||||
pub struct PreferencesSaveRequest {
|
||||
#[serde(default)]
|
||||
pub values: Vec<PreferenceValue>,
|
||||
}
|
||||
|
||||
/// Remove allowlisted user preferences.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/preferences/remove", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PreferencesRemoveRequest {
|
||||
#[serde(default)]
|
||||
pub keys: Vec<PreferenceKey>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PreferenceKey {
|
||||
#[default]
|
||||
AutoCompactThreshold,
|
||||
VoiceAutoSubmitPhrases,
|
||||
VoiceDictationProvider,
|
||||
VoiceDictationPreferredMic,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PreferenceValue {
|
||||
pub key: PreferenceKey,
|
||||
#[serde(default)]
|
||||
pub value: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Upsert a single non-secret config value.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/config/upsert", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpsertConfigRequest {
|
||||
pub key: String,
|
||||
pub value: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Remove a single non-secret config value.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/config/remove", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveConfigRequest {
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
/// Check whether a secret exists. Never returns the actual value.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/secret/check", response = CheckSecretResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CheckSecretRequest {
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
/// Secret check response.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CheckSecretResponse {
|
||||
pub exists: bool,
|
||||
pub struct PreferencesReadResponse {
|
||||
pub values: Vec<PreferenceValue>,
|
||||
}
|
||||
|
||||
/// Set a secret value (write-only).
|
||||
/// Read Goose default provider and model configuration.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/secret/upsert", response = EmptyResponse)]
|
||||
#[request(method = "_goose/defaults/read", response = DefaultsReadResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpsertSecretRequest {
|
||||
pub key: String,
|
||||
pub value: serde_json::Value,
|
||||
pub struct DefaultsReadRequest {}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DefaultsReadResponse {
|
||||
pub provider_id: Option<String>,
|
||||
pub model_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Remove a secret.
|
||||
/// Set a dictation provider secret value.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/secret/remove", response = EmptyResponse)]
|
||||
#[request(method = "_goose/dictation/secret/save", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveSecretRequest {
|
||||
pub key: String,
|
||||
pub struct DictationSecretSaveRequest {
|
||||
pub provider: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// Remove a dictation provider secret value.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/dictation/secret/delete", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DictationSecretDeleteRequest {
|
||||
pub provider: String,
|
||||
}
|
||||
|
||||
/// Update the project association for a session.
|
||||
|
||||
+20
-20
@@ -121,34 +121,24 @@
|
||||
"responseType": "ProviderConfigChangeResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/config/read",
|
||||
"requestType": "ReadConfigRequest",
|
||||
"responseType": "ReadConfigResponse"
|
||||
"method": "_goose/preferences/read",
|
||||
"requestType": "PreferencesReadRequest",
|
||||
"responseType": "PreferencesReadResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/config/upsert",
|
||||
"requestType": "UpsertConfigRequest",
|
||||
"method": "_goose/preferences/save",
|
||||
"requestType": "PreferencesSaveRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/config/remove",
|
||||
"requestType": "RemoveConfigRequest",
|
||||
"method": "_goose/preferences/remove",
|
||||
"requestType": "PreferencesRemoveRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/secret/check",
|
||||
"requestType": "CheckSecretRequest",
|
||||
"responseType": "CheckSecretResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/secret/upsert",
|
||||
"requestType": "UpsertSecretRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/secret/remove",
|
||||
"requestType": "RemoveSecretRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
"method": "_goose/defaults/read",
|
||||
"requestType": "DefaultsReadRequest",
|
||||
"responseType": "DefaultsReadResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/session/export",
|
||||
@@ -220,6 +210,16 @@
|
||||
"requestType": "DictationConfigRequest",
|
||||
"responseType": "DictationConfigResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/dictation/secret/save",
|
||||
"requestType": "DictationSecretSaveRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/dictation/secret/delete",
|
||||
"requestType": "DictationSecretDeleteRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/dictation/models/list",
|
||||
"requestType": "DictationModelsListRequest",
|
||||
|
||||
+153
-125
@@ -1246,118 +1246,114 @@
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/providers/config/delete"
|
||||
},
|
||||
"ReadConfigRequest": {
|
||||
"PreferencesReadRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
"keys": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/PreferenceKey"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"description": "Read allowlisted user preferences. Empty `keys` means all supported preferences.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/preferences/read"
|
||||
},
|
||||
"PreferenceKey": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"autoCompactThreshold",
|
||||
"voiceAutoSubmitPhrases",
|
||||
"voiceDictationProvider",
|
||||
"voiceDictationPreferredMic"
|
||||
]
|
||||
},
|
||||
"PreferencesReadResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"values": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/PreferenceValue"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
"values"
|
||||
],
|
||||
"description": "Read a single non-secret config value.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/config/read"
|
||||
"x-method": "_goose/preferences/read"
|
||||
},
|
||||
"ReadConfigResponse": {
|
||||
"PreferenceValue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"$ref": "#/$defs/PreferenceKey"
|
||||
},
|
||||
"value": {
|
||||
"default": null
|
||||
}
|
||||
},
|
||||
"description": "Config read response.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/config/read"
|
||||
"required": [
|
||||
"key"
|
||||
]
|
||||
},
|
||||
"UpsertConfigRequest": {
|
||||
"PreferencesSaveRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
"values": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/PreferenceValue"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"description": "Save allowlisted user preferences.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/preferences/save"
|
||||
},
|
||||
"PreferencesRemoveRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"keys": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/PreferenceKey"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"description": "Remove allowlisted user preferences.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/preferences/remove"
|
||||
},
|
||||
"DefaultsReadRequest": {
|
||||
"type": "object",
|
||||
"description": "Read Goose default provider and model configuration.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/defaults/read"
|
||||
},
|
||||
"DefaultsReadResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"providerId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"value": {}
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
"value"
|
||||
],
|
||||
"description": "Upsert a single non-secret config value.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/config/upsert"
|
||||
},
|
||||
"RemoveConfigRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
"modelId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
],
|
||||
"description": "Remove a single non-secret config value.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/config/remove"
|
||||
},
|
||||
"CheckSecretRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
],
|
||||
"description": "Check whether a secret exists. Never returns the actual value.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/secret/check"
|
||||
},
|
||||
"CheckSecretResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"exists": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"exists"
|
||||
],
|
||||
"description": "Secret check response.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/secret/check"
|
||||
},
|
||||
"UpsertSecretRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {}
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
"value"
|
||||
],
|
||||
"description": "Set a secret value (write-only).",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/secret/upsert"
|
||||
},
|
||||
"RemoveSecretRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
],
|
||||
"description": "Remove a secret.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/secret/remove"
|
||||
"x-method": "_goose/defaults/read"
|
||||
},
|
||||
"ExportSessionRequest": {
|
||||
"type": "object",
|
||||
@@ -1920,6 +1916,38 @@
|
||||
"description"
|
||||
]
|
||||
},
|
||||
"DictationSecretSaveRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"value"
|
||||
],
|
||||
"description": "Set a dictation provider secret value.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/dictation/secret/save"
|
||||
},
|
||||
"DictationSecretDeleteRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider"
|
||||
],
|
||||
"description": "Remove a dictation provider secret value.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/dictation/secret/delete"
|
||||
},
|
||||
"DictationModelsListRequest": {
|
||||
"type": "object",
|
||||
"description": "List available local Whisper models with their download status.",
|
||||
@@ -2330,56 +2358,38 @@
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/ReadConfigRequest"
|
||||
"$ref": "#/$defs/PreferencesReadRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/config/read",
|
||||
"title": "ReadConfigRequest"
|
||||
"description": "Params for _goose/preferences/read",
|
||||
"title": "PreferencesReadRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/UpsertConfigRequest"
|
||||
"$ref": "#/$defs/PreferencesSaveRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/config/upsert",
|
||||
"title": "UpsertConfigRequest"
|
||||
"description": "Params for _goose/preferences/save",
|
||||
"title": "PreferencesSaveRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/RemoveConfigRequest"
|
||||
"$ref": "#/$defs/PreferencesRemoveRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/config/remove",
|
||||
"title": "RemoveConfigRequest"
|
||||
"description": "Params for _goose/preferences/remove",
|
||||
"title": "PreferencesRemoveRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CheckSecretRequest"
|
||||
"$ref": "#/$defs/DefaultsReadRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/secret/check",
|
||||
"title": "CheckSecretRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/UpsertSecretRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/secret/upsert",
|
||||
"title": "UpsertSecretRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/RemoveSecretRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/secret/remove",
|
||||
"title": "RemoveSecretRequest"
|
||||
"description": "Params for _goose/defaults/read",
|
||||
"title": "DefaultsReadRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
@@ -2507,6 +2517,24 @@
|
||||
"description": "Params for _goose/dictation/config",
|
||||
"title": "DictationConfigRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/DictationSecretSaveRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/dictation/secret/save",
|
||||
"title": "DictationSecretSaveRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/DictationSecretDeleteRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/dictation/secret/delete",
|
||||
"title": "DictationSecretDeleteRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2730,18 +2758,18 @@
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/ReadConfigResponse"
|
||||
"$ref": "#/$defs/PreferencesReadResponse"
|
||||
}
|
||||
],
|
||||
"title": "ReadConfigResponse"
|
||||
"title": "PreferencesReadResponse"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CheckSecretResponse"
|
||||
"$ref": "#/$defs/DefaultsReadResponse"
|
||||
}
|
||||
],
|
||||
"title": "CheckSecretResponse"
|
||||
"title": "DefaultsReadResponse"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
|
||||
@@ -74,7 +74,6 @@ mod dispatch;
|
||||
mod extensions;
|
||||
mod providers;
|
||||
mod resources;
|
||||
mod secrets;
|
||||
mod sessions;
|
||||
mod sources;
|
||||
mod tools;
|
||||
|
||||
@@ -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())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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 {})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -69,7 +69,7 @@ fn mock_provider_factory() -> AcpProviderFactory {
|
||||
|
||||
#[test]
|
||||
fn test_custom_get_tools() {
|
||||
run_test(async {
|
||||
run_test(async move {
|
||||
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
|
||||
let mut conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;
|
||||
|
||||
@@ -92,7 +92,7 @@ fn test_custom_get_tools() {
|
||||
|
||||
#[test]
|
||||
fn test_custom_get_extensions() {
|
||||
run_test(async {
|
||||
run_test(async move {
|
||||
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
|
||||
let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;
|
||||
|
||||
@@ -140,53 +140,289 @@ fn test_custom_provider_inventory_includes_metadata() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_config_crud() {
|
||||
fn test_custom_preferences_read_save_remove() {
|
||||
run_test(async {
|
||||
let data_root = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
data_root
|
||||
.path()
|
||||
.join(goose::config::base::CONFIG_YAML_NAME),
|
||||
"GOOSE_MODEL: gpt-4o\nGOOSE_PROVIDER: openai\nGOOSE_AUTO_COMPACT_THRESHOLD: 0.7\nVOICE_AUTO_SUBMIT_PHRASES: send it\n",
|
||||
)
|
||||
.unwrap();
|
||||
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
|
||||
let config = TestConnectionConfig {
|
||||
data_root: data_root.path().to_path_buf(),
|
||||
..Default::default()
|
||||
};
|
||||
let conn = AcpServerConnection::new(config, openai).await;
|
||||
|
||||
let response = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/preferences/read",
|
||||
serde_json::json!({
|
||||
"keys": [
|
||||
"autoCompactThreshold",
|
||||
"voiceAutoSubmitPhrases",
|
||||
"voiceDictationPreferredMic"
|
||||
],
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("preferences read should succeed");
|
||||
assert_eq!(
|
||||
response.get("values"),
|
||||
Some(&serde_json::json!([
|
||||
{ "key": "autoCompactThreshold", "value": 0.7 },
|
||||
{ "key": "voiceAutoSubmitPhrases", "value": "send it" },
|
||||
{ "key": "voiceDictationPreferredMic", "value": null },
|
||||
]))
|
||||
);
|
||||
|
||||
send_custom(
|
||||
conn.cx(),
|
||||
"_goose/preferences/save",
|
||||
serde_json::json!({
|
||||
"values": [
|
||||
{ "key": "voiceDictationProvider", "value": "__disabled__" },
|
||||
{ "key": "voiceDictationPreferredMic", "value": "mic-1" }
|
||||
],
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("preferences save should succeed");
|
||||
|
||||
send_custom(
|
||||
conn.cx(),
|
||||
"_goose/preferences/remove",
|
||||
serde_json::json!({
|
||||
"keys": ["voiceDictationProvider"],
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("preferences remove should succeed");
|
||||
|
||||
let response = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/preferences/read",
|
||||
serde_json::json!({
|
||||
"keys": ["voiceDictationProvider", "voiceDictationPreferredMic"],
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("preferences read after remove should succeed");
|
||||
assert_eq!(
|
||||
response.get("values"),
|
||||
Some(&serde_json::json!([
|
||||
{ "key": "voiceDictationProvider", "value": null },
|
||||
{ "key": "voiceDictationPreferredMic", "value": "mic-1" },
|
||||
]))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_preferences_save_rejects_invalid_values() {
|
||||
run_test(async {
|
||||
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
|
||||
let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;
|
||||
|
||||
let invalid_payloads = [
|
||||
serde_json::json!({
|
||||
"values": [{ "key": "autoCompactThreshold", "value": 0 }],
|
||||
}),
|
||||
serde_json::json!({
|
||||
"values": [{ "key": "autoCompactThreshold", "value": 1.1 }],
|
||||
}),
|
||||
serde_json::json!({
|
||||
"values": [{ "key": "voiceAutoSubmitPhrases", "value": ["send"] }],
|
||||
}),
|
||||
serde_json::json!({
|
||||
"values": [{ "key": "voiceDictationProvider", "value": "bogus" }],
|
||||
}),
|
||||
serde_json::json!({
|
||||
"values": [{ "key": "voiceDictationPreferredMic", "value": "" }],
|
||||
}),
|
||||
];
|
||||
|
||||
for payload in invalid_payloads {
|
||||
let result = send_custom(conn.cx(), "_goose/preferences/save", payload).await;
|
||||
assert!(result.is_err(), "expected invalid params error");
|
||||
}
|
||||
|
||||
let result = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/preferences/save",
|
||||
serde_json::json!({
|
||||
"values": [
|
||||
{ "key": "voiceDictationPreferredMic", "value": "mic-1" },
|
||||
{ "key": "voiceDictationProvider", "value": "bogus" }
|
||||
],
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_err(), "expected invalid params error");
|
||||
|
||||
let response = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/preferences/read",
|
||||
serde_json::json!({
|
||||
"keys": ["voiceDictationPreferredMic"],
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("preferences read should succeed");
|
||||
assert_eq!(
|
||||
response.get("values"),
|
||||
Some(&serde_json::json!([
|
||||
{ "key": "voiceDictationPreferredMic", "value": null },
|
||||
]))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_defaults_read() {
|
||||
run_test(async {
|
||||
let data_root = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
data_root.path().join(goose::config::base::CONFIG_YAML_NAME),
|
||||
"GOOSE_MODEL: claude-3-5-haiku-latest\nGOOSE_PROVIDER: anthropic\n",
|
||||
)
|
||||
.unwrap();
|
||||
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
|
||||
let config = TestConnectionConfig {
|
||||
data_root: data_root.path().to_path_buf(),
|
||||
..Default::default()
|
||||
};
|
||||
let conn = AcpServerConnection::new(config, openai).await;
|
||||
|
||||
let response = send_custom(conn.cx(), "_goose/defaults/read", serde_json::json!({}))
|
||||
.await
|
||||
.expect("defaults read should succeed");
|
||||
assert_eq!(
|
||||
response,
|
||||
serde_json::json!({
|
||||
"providerId": "anthropic",
|
||||
"modelId": "claude-3-5-haiku-latest",
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_dictation_secret_save_delete() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let root_path = root.path().to_string_lossy().to_string();
|
||||
let _env = env_lock::lock_env([
|
||||
("GOOSE_PATH_ROOT", Some(root_path.as_str())),
|
||||
("GOOSE_DISABLE_KEYRING", Some("1")),
|
||||
("GROQ_API_KEY", None::<&str>),
|
||||
]);
|
||||
let config_dir = goose::config::paths::Paths::config_dir();
|
||||
std::fs::create_dir_all(&config_dir).unwrap();
|
||||
std::fs::write(
|
||||
config_dir.join(goose::config::base::CONFIG_YAML_NAME),
|
||||
"GOOSE_MODEL: gpt-4o\nGOOSE_PROVIDER: openai\nGOOSE_DISABLE_KEYRING: true\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
run_test(async move {
|
||||
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
|
||||
let config = TestConnectionConfig {
|
||||
data_root: config_dir.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let conn = AcpServerConnection::new(config, openai).await;
|
||||
|
||||
send_custom(
|
||||
conn.cx(),
|
||||
"_goose/dictation/secret/save",
|
||||
serde_json::json!({
|
||||
"provider": "groq",
|
||||
"value": "groq-key",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("dictation secret save should succeed");
|
||||
|
||||
let config = send_custom(conn.cx(), "_goose/dictation/config", serde_json::json!({}))
|
||||
.await
|
||||
.expect("dictation config should succeed");
|
||||
assert_eq!(
|
||||
config
|
||||
.pointer("/providers/groq/configured")
|
||||
.and_then(|value| value.as_bool()),
|
||||
Some(true)
|
||||
);
|
||||
|
||||
let provider_config_result = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/dictation/secret/save",
|
||||
serde_json::json!({
|
||||
"provider": "openai",
|
||||
"value": "openai-key",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
provider_config_result.is_err(),
|
||||
"provider-config dictation providers should be rejected"
|
||||
);
|
||||
|
||||
let unknown_result = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/dictation/secret/save",
|
||||
serde_json::json!({
|
||||
"provider": "unknown",
|
||||
"value": "key",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
unknown_result.is_err(),
|
||||
"unknown provider should be rejected"
|
||||
);
|
||||
|
||||
send_custom(
|
||||
conn.cx(),
|
||||
"_goose/dictation/secret/delete",
|
||||
serde_json::json!({
|
||||
"provider": "groq",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("dictation secret delete should succeed");
|
||||
|
||||
let config = send_custom(conn.cx(), "_goose/dictation/config", serde_json::json!({}))
|
||||
.await
|
||||
.expect("dictation config should succeed");
|
||||
assert_eq!(
|
||||
config
|
||||
.pointer("/providers/groq/configured")
|
||||
.and_then(|value| value.as_bool()),
|
||||
Some(false)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_raw_config_and_secret_methods_are_removed() {
|
||||
run_test(async {
|
||||
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
|
||||
let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;
|
||||
|
||||
for method in [
|
||||
"_goose/config/read",
|
||||
"_goose/config/upsert",
|
||||
serde_json::json!({
|
||||
"key": "GOOSE_PROVIDER",
|
||||
"value": "anthropic",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("config upsert should succeed");
|
||||
|
||||
let response = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/config/read",
|
||||
serde_json::json!({
|
||||
"key": "GOOSE_PROVIDER",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("config read should succeed");
|
||||
assert_eq!(response.get("value"), Some(&serde_json::json!("anthropic")));
|
||||
|
||||
send_custom(
|
||||
conn.cx(),
|
||||
"_goose/config/remove",
|
||||
serde_json::json!({
|
||||
"key": "GOOSE_PROVIDER",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("config remove should succeed");
|
||||
|
||||
let response = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/config/read",
|
||||
serde_json::json!({
|
||||
"key": "GOOSE_PROVIDER",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("config read after remove should succeed");
|
||||
assert_eq!(response.get("value"), Some(&serde_json::Value::Null));
|
||||
"_goose/secret/check",
|
||||
"_goose/secret/upsert",
|
||||
"_goose/secret/remove",
|
||||
] {
|
||||
let result = send_custom(conn.cx(), method, serde_json::json!({})).await;
|
||||
assert!(result.is_err(), "{method} should be removed");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ fn acp_secret_mutations_and_inventory_refresh_invalidate_global_secret_cache() {
|
||||
("GOOSE_PATH_ROOT", Some(root_path.as_str())),
|
||||
("GOOSE_DISABLE_KEYRING", Some("1")),
|
||||
("ANTHROPIC_API_KEY", None),
|
||||
("GROQ_API_KEY", None),
|
||||
("OPENAI_API_KEY", None),
|
||||
("XAI_API_KEY", None),
|
||||
("XAI_HOST", None),
|
||||
@@ -87,12 +88,12 @@ fn acp_secret_mutations_and_inventory_refresh_invalidate_global_secret_cache() {
|
||||
let config_dir = Paths::config_dir();
|
||||
let data_dir = Paths::data_dir();
|
||||
write_config(&config_dir);
|
||||
write_secrets(&config_dir, "OPENAI_API_KEY: stale-key\n");
|
||||
write_secrets(&config_dir, "GROQ_API_KEY: stale-key\n");
|
||||
|
||||
run_test(async move {
|
||||
assert_eq!(
|
||||
Config::global()
|
||||
.get_secret::<String>("OPENAI_API_KEY")
|
||||
.get_secret::<String>("GROQ_API_KEY")
|
||||
.unwrap(),
|
||||
"stale-key"
|
||||
);
|
||||
@@ -109,43 +110,43 @@ fn acp_secret_mutations_and_inventory_refresh_invalidate_global_secret_cache() {
|
||||
};
|
||||
let conn = AcpServerConnection::new(config, openai).await;
|
||||
|
||||
write_secrets(&config_dir, "OPENAI_API_KEY: fresh-key\n");
|
||||
write_secrets(&config_dir, "GROQ_API_KEY: fresh-key\n");
|
||||
send_custom(
|
||||
conn.cx(),
|
||||
"_goose/secret/upsert",
|
||||
"_goose/dictation/secret/save",
|
||||
serde_json::json!({
|
||||
"key": "OPENAI_API_KEY",
|
||||
"provider": "groq",
|
||||
"value": "fresh-key",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("secret upsert should succeed");
|
||||
.expect("dictation secret save should succeed");
|
||||
|
||||
assert_eq!(
|
||||
Config::global()
|
||||
.get_secret::<String>("OPENAI_API_KEY")
|
||||
.get_secret::<String>("GROQ_API_KEY")
|
||||
.unwrap(),
|
||||
"fresh-key",
|
||||
"ACP secret upsert should invalidate the global secrets cache"
|
||||
"ACP dictation secret save should invalidate the global secrets cache"
|
||||
);
|
||||
|
||||
write_secrets(&config_dir, "{}\n");
|
||||
send_custom(
|
||||
conn.cx(),
|
||||
"_goose/secret/remove",
|
||||
"_goose/dictation/secret/delete",
|
||||
serde_json::json!({
|
||||
"key": "OPENAI_API_KEY",
|
||||
"provider": "groq",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("secret remove should succeed");
|
||||
.expect("dictation secret delete should succeed");
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
Config::global().get_secret::<String>("OPENAI_API_KEY"),
|
||||
Config::global().get_secret::<String>("GROQ_API_KEY"),
|
||||
Err(ConfigError::NotFound(_))
|
||||
),
|
||||
"ACP secret remove should invalidate the global secrets cache"
|
||||
"ACP dictation secret delete should invalidate the global secrets cache"
|
||||
);
|
||||
|
||||
let save_provider_config = send_custom(
|
||||
|
||||
Reference in New Issue
Block a user