Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/goose-server/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ use utoipa::OpenApi;
#[openapi(
paths(
super::routes::config_management::backup_config,
super::routes::config_management::recover_config,
super::routes::config_management::validate_config,
super::routes::config_management::init_config,
super::routes::config_management::upsert_config,
super::routes::config_management::remove_config,
Expand Down
127 changes: 88 additions & 39 deletions crates/goose-server/src/routes/config_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,44 +334,15 @@ pub async fn init_config(
return Ok(Json("Config already exists".to_string()));
}

let workspace_root = match std::env::current_exe() {
Ok(mut exe_path) => {
while let Some(parent) = exe_path.parent() {
let cargo_toml = parent.join("Cargo.toml");
if cargo_toml.exists() {
if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
if content.contains("[workspace]") {
exe_path = parent.to_path_buf();
break;
}
}
}
exe_path = parent.to_path_buf();
}
exe_path
}
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
};

let init_config_path = workspace_root.join("init-config.yaml");
if !init_config_path.exists() {
return Ok(Json(
// Use the shared function to load init-config.yaml
match goose::config::base::load_init_config_from_workspace() {
Ok(init_values) => match config.save_values(init_values) {
Ok(_) => Ok(Json("Config initialized successfully".to_string())),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
},
Err(_) => Ok(Json(
"No init-config.yaml found, using default configuration".to_string(),
));
}

let init_content = match std::fs::read_to_string(&init_config_path) {
Ok(content) => content,
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
};
let init_values: HashMap<String, Value> = match serde_yaml::from_str(&init_content) {
Ok(values) => values,
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
};

match config.save_values(init_values) {
Ok(_) => Ok(Json("Config initialized successfully".to_string())),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
)),
}
}

Expand Down Expand Up @@ -432,15 +403,91 @@ pub async fn backup_config(
backup_name.push(".bak");

let backup = config_path.with_file_name(backup_name);
match std::fs::rename(&config_path, &backup) {
Ok(_) => Ok(Json(format!("Moved {:?} to {:?}", config_path, backup))),
match std::fs::copy(&config_path, &backup) {
Ok(_) => Ok(Json(format!("Copied {:?} to {:?}", config_path, backup))),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
} else {
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}

#[utoipa::path(
post,
path = "/config/recover",
responses(
(status = 200, description = "Config recovery attempted", body = String),
(status = 500, description = "Internal server error")
)
)]
pub async fn recover_config(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<String>, StatusCode> {
verify_secret_key(&headers, &state)?;

let config = Config::global();

// Force a reload which will trigger recovery if needed
match config.load_values() {
Ok(values) => {
let recovered_keys: Vec<String> = values.keys().cloned().collect();
if recovered_keys.is_empty() {
Ok(Json("Config recovery completed, but no data was recoverable. Starting with empty configuration.".to_string()))
} else {
Ok(Json(format!(
"Config recovery completed. Recovered {} keys: {}",
recovered_keys.len(),
recovered_keys.join(", ")
)))
}
}
Err(e) => {
tracing::error!("Config recovery failed: {}", e);
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}

#[utoipa::path(
get,
path = "/config/validate",
responses(
(status = 200, description = "Config validation result", body = String),
(status = 422, description = "Config file is corrupted")
)
)]
pub async fn validate_config(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<String>, StatusCode> {
verify_secret_key(&headers, &state)?;

let config_dir = choose_app_strategy(APP_STRATEGY.clone())
.expect("goose requires a home dir")
.config_dir();

let config_path = config_dir.join("config.yaml");

if !config_path.exists() {
return Ok(Json("Config file does not exist".to_string()));
}

match std::fs::read_to_string(&config_path) {
Ok(content) => match serde_yaml::from_str::<serde_yaml::Value>(&content) {
Ok(_) => Ok(Json("Config file is valid".to_string())),
Err(e) => {
tracing::warn!("Config validation failed: {}", e);
Err(StatusCode::UNPROCESSABLE_ENTITY)
}
},
Err(e) => {
tracing::error!("Failed to read config file: {}", e);
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}

#[utoipa::path(
get,
path = "/config/current-model",
Expand Down Expand Up @@ -473,6 +520,8 @@ pub fn routes(state: Arc<AppState>) -> Router {
.route("/config/providers", get(providers))
.route("/config/init", post(init_config))
.route("/config/backup", post(backup_config))
.route("/config/recover", post(recover_config))
.route("/config/validate", get(validate_config))
.route("/config/permissions", post(upsert_permissions))
.route("/config/current-model", get(get_current_model))
.with_state(state)
Expand Down
Loading
Loading