From 42bf8043e2c8e06ab65eca725bf2744f749bbad1 Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Fri, 31 Oct 2025 14:12:21 -0400 Subject: [PATCH] Expose everything in the API too --- Cargo.lock | 1 + crates/goose-server/Cargo.toml | 1 + crates/goose-server/src/openapi.rs | 11 + crates/goose-server/src/routes/database.rs | 398 ++++++++++++++++++++- ui/desktop/openapi.json | 296 +++++++++++++++ ui/desktop/src/api/sdk.gen.ts | 42 ++- ui/desktop/src/api/types.gen.ts | 197 ++++++++++ 7 files changed, 942 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 412546e644..02ebf79e7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2882,6 +2882,7 @@ dependencies = [ "goose", "goose-mcp", "http 1.2.0", + "once_cell", "reqwest 0.12.12", "rmcp", "schemars", diff --git a/crates/goose-server/Cargo.toml b/crates/goose-server/Cargo.toml index 59d4f20418..938d898bd1 100644 --- a/crates/goose-server/Cargo.toml +++ b/crates/goose-server/Cargo.toml @@ -39,6 +39,7 @@ reqwest = { version = "0.12.9", features = ["json", "rustls-tls", "blocking", "m tokio-util = "0.7.15" uuid = { version = "1.11", features = ["v4"] } serde_path_to_error = "0.1.20" +once_cell = "1.19" [[bin]] name = "goosed" diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index 9432597998..c614bc9167 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -327,6 +327,10 @@ derive_utoipa!(Icon as IconSchema); super::routes::status::status, super::routes::status::diagnostics, super::routes::database::database_status, + super::routes::database::create_backup, + super::routes::database::list_backups, + super::routes::database::delete_backups, + super::routes::database::restore_backup, super::routes::config_management::backup_config, super::routes::config_management::recover_config, super::routes::config_management::validate_config, @@ -447,6 +451,13 @@ derive_utoipa!(Icon as IconSchema); SessionInsights, super::routes::database::DatabaseStatusResponse, super::routes::database::BackupInfo, + super::routes::database::CreateBackupRequest, + super::routes::database::CreateBackupResponse, + super::routes::database::BackupsListResponse, + super::routes::database::RestoreBackupRequest, + super::routes::database::RestoreBackupResponse, + super::routes::database::DeleteBackupsRequest, + super::routes::database::DeleteBackupsResponse, Conversation, IconSchema, goose::session::extension_data::ExtensionData, diff --git a/crates/goose-server/src/routes/database.rs b/crates/goose-server/src/routes/database.rs index 73cd14bbe6..c31fe60000 100644 --- a/crates/goose-server/src/routes/database.rs +++ b/crates/goose-server/src/routes/database.rs @@ -1,9 +1,23 @@ -use axum::{http::StatusCode, routing::get, Json, Router}; +use axum::{ + http::StatusCode, + routing::{delete, get, post}, + Json, Router, +}; use chrono::{DateTime, Utc}; +use goose::config::paths::Paths; use goose::session::session_manager::SessionManager; +use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::sync::Mutex; use utoipa::ToSchema; +use crate::routes::errors::ErrorResponse; + +static BACKUP_MUTEX: Lazy>> = Lazy::new(|| Arc::new(Mutex::new(()))); +static RESTORE_MUTEX: Lazy>> = Lazy::new(|| Arc::new(Mutex::new(()))); + #[derive(Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct BackupInfo { @@ -13,6 +27,69 @@ pub struct BackupInfo { pub schema_version: Option, } +#[derive(Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreateBackupRequest { + /// Optional custom name for the backup + pub name: Option, +} + +#[derive(Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreateBackupResponse { + /// Filename of the created backup + pub filename: String, + /// Size of the backup file in bytes + pub size: u64, + /// When the backup was created + pub created_at: DateTime, +} + +#[derive(Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct BackupsListResponse { + /// List of available backups + pub backups: Vec, +} + +#[derive(Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct RestoreBackupRequest { + /// Filename of the backup to restore + pub filename: String, +} + +#[derive(Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct RestoreBackupResponse { + /// Success message + pub message: String, + /// Backup that was restored + pub restored_from: String, +} + +#[derive(Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DeleteBackupsRequest { + /// List of backup filenames to delete + pub filenames: Vec, + /// Delete all backups if true + pub delete_all: bool, + /// Clean up orphaned WAL/SHM files + pub cleanup_orphaned: bool, +} + +#[derive(Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DeleteBackupsResponse { + /// Successfully deleted files + pub deleted: Vec, + /// Files that failed to delete + pub failed: Vec, + /// Number of orphaned files cleaned + pub orphaned_cleaned: usize, +} + #[derive(Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct DatabaseStatusResponse { @@ -75,6 +152,321 @@ pub async fn database_status() -> Result, StatusCod })) } -pub fn routes() -> Router { - Router::new().route("/database/status", get(database_status)) +#[utoipa::path( + post, + path = "/database/backup", + request_body = CreateBackupRequest, + responses( + (status = 201, description = "Backup created successfully", body = CreateBackupResponse), + (status = 401, description = "Unauthorized - Invalid or missing API key"), + (status = 500, description = "Internal server error") + ), + security( + ("api_key" = []) + ), + tag = "Database Management" +)] +pub async fn create_backup( + Json(request): Json, +) -> Result<(StatusCode, Json), ErrorResponse> { + let _lock = BACKUP_MUTEX.try_lock().map_err(|_| ErrorResponse { + message: "A backup operation is already in progress".to_string(), + status: StatusCode::CONFLICT, + })?; + + let backup_path = SessionManager::create_backup(request.name) + .await + .map_err(|e| ErrorResponse { + message: format!("Failed to create backup: {}", e), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + + let filename = backup_path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown") + .to_string(); + + let metadata = tokio::fs::metadata(&backup_path) + .await + .map_err(|e| ErrorResponse { + message: format!("Failed to read backup file metadata: {}", e), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + + let size = metadata.len(); + let created_at = Utc::now(); + + Ok(( + StatusCode::CREATED, + Json(CreateBackupResponse { + filename, + size, + created_at, + }), + )) +} + +#[utoipa::path( + get, + path = "/database/backups", + responses( + (status = 200, description = "List of backups retrieved successfully", body = BackupsListResponse), + (status = 401, description = "Unauthorized - Invalid or missing API key"), + (status = 500, description = "Internal server error") + ), + security( + ("api_key" = []) + ), + tag = "Database Management" +)] +pub async fn list_backups() -> Result, ErrorResponse> { + let backups = SessionManager::list_backups() + .await + .map_err(|e| ErrorResponse { + message: format!("Failed to list backups: {}", e), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + + let sanitized_backups = backups + .into_iter() + .map(|backup| { + let filename = backup + .path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown") + .to_string(); + + BackupInfo { + filename, + created_at: backup.created_at, + size: backup.size, + schema_version: backup.schema_version, + } + }) + .collect(); + + Ok(Json(BackupsListResponse { + backups: sanitized_backups, + })) +} + +#[utoipa::path( + post, + path = "/database/restore", + request_body = RestoreBackupRequest, + responses( + (status = 200, description = "Database restored successfully", body = RestoreBackupResponse), + (status = 400, description = "Bad request - Invalid backup filename"), + (status = 401, description = "Unauthorized - Invalid or missing API key"), + (status = 404, description = "Backup not found"), + (status = 500, description = "Internal server error") + ), + security( + ("api_key" = []) + ), + tag = "Database Management" +)] +pub async fn restore_backup( + Json(request): Json, +) -> Result, ErrorResponse> { + let _lock = RESTORE_MUTEX.try_lock().map_err(|_| ErrorResponse { + message: "A restore operation is already in progress".to_string(), + status: StatusCode::CONFLICT, + })?; + + let filename = request.filename.trim(); + if filename.is_empty() { + return Err(ErrorResponse { + message: "Backup filename cannot be empty".to_string(), + status: StatusCode::BAD_REQUEST, + }); + } + + let backup_dir = Paths::backup_dir(); + let backup_path = backup_dir.join(filename); + + let canonical_backup = backup_path.canonicalize().map_err(|_| ErrorResponse { + message: format!("Backup '{}' not found", filename), + status: StatusCode::NOT_FOUND, + })?; + let canonical_backup_dir = backup_dir.canonicalize().map_err(|e| ErrorResponse { + message: format!("Failed to access backup directory: {}", e), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + + if !canonical_backup.starts_with(&canonical_backup_dir) { + tracing::warn!("Path traversal attempt detected: {}", filename); + return Err(ErrorResponse { + message: "Invalid backup path".to_string(), + status: StatusCode::FORBIDDEN, + }); + } + + if !canonical_backup.exists() { + return Err(ErrorResponse { + message: format!("Backup '{}' not found", filename), + status: StatusCode::NOT_FOUND, + }); + } + + SessionManager::restore_backup(&canonical_backup) + .await + .map_err(|e| ErrorResponse { + message: format!("Failed to restore backup: {}", e), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + + Ok(Json(RestoreBackupResponse { + message: "Database restored successfully. Server restart required.".to_string(), + restored_from: filename.to_string(), + })) +} + +#[utoipa::path( + delete, + path = "/database/backups/delete", + request_body = DeleteBackupsRequest, + responses( + (status = 200, description = "Backups deleted successfully", body = DeleteBackupsResponse), + (status = 400, description = "Bad request - Invalid parameters"), + (status = 401, description = "Unauthorized - Invalid or missing API key"), + (status = 500, description = "Internal server error") + ), + security( + ("api_key" = []) + ), + tag = "Database Management" +)] +pub async fn delete_backups( + Json(request): Json, +) -> Result, ErrorResponse> { + let _lock = BACKUP_MUTEX.try_lock().map_err(|_| ErrorResponse { + message: "A backup operation is in progress, cannot delete".to_string(), + status: StatusCode::CONFLICT, + })?; + + if request.delete_all && !request.filenames.is_empty() { + return Err(ErrorResponse { + message: "Cannot specify both deleteAll and specific filenames".to_string(), + status: StatusCode::BAD_REQUEST, + }); + } + + if !request.delete_all && request.filenames.is_empty() { + return Err(ErrorResponse { + message: "Must specify either deleteAll or at least one filename".to_string(), + status: StatusCode::BAD_REQUEST, + }); + } + + let backup_dir = Paths::backup_dir(); + + let files_to_delete: Vec = if request.delete_all { + let backups = SessionManager::list_backups() + .await + .map_err(|e| ErrorResponse { + message: format!("Failed to list backups: {}", e), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + backups.into_iter().map(|b| b.path).collect() + } else { + let mut resolved_paths = Vec::new(); + for filename in &request.filenames { + let path = backup_dir.join(filename); + if path.exists() { + let canonical = path.canonicalize().map_err(|e| ErrorResponse { + message: format!("Failed to resolve backup path: {}", e), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + let canonical_backup_dir = + backup_dir.canonicalize().map_err(|e| ErrorResponse { + message: format!("Failed to access backup directory: {}", e), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + + if !canonical.starts_with(&canonical_backup_dir) { + tracing::warn!("Path traversal attempt detected: {}", filename); + return Err(ErrorResponse { + message: "Invalid backup path".to_string(), + status: StatusCode::FORBIDDEN, + }); + } + resolved_paths.push(canonical); + } + } + resolved_paths + }; + + let mut deleted = Vec::new(); + let mut failed = Vec::new(); + + for path in files_to_delete { + let filename = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown") + .to_string(); + + match tokio::fs::remove_file(&path).await { + Ok(_) => { + deleted.push(filename.clone()); + + let wal_path = path.with_extension("db-wal"); + if wal_path.exists() { + let _ = tokio::fs::remove_file(&wal_path).await; + } + + let shm_path = path.with_extension("db-shm"); + if shm_path.exists() { + let _ = tokio::fs::remove_file(&shm_path).await; + } + } + Err(_) => { + failed.push(filename); + } + } + } + + let mut orphaned_cleaned = 0; + if request.cleanup_orphaned { + orphaned_cleaned = cleanup_orphaned_files(&backup_dir).await; + } + + Ok(Json(DeleteBackupsResponse { + deleted, + failed, + orphaned_cleaned, + })) +} + +async fn cleanup_orphaned_files(backup_dir: &Path) -> usize { + let mut cleaned = 0; + + if let Ok(mut entries) = tokio::fs::read_dir(backup_dir).await { + while let Ok(Some(entry)) = entries.next_entry().await { + let path = entry.path(); + if let Some(extension) = path.extension() { + let ext_str = extension.to_string_lossy(); + if ext_str == "db-wal" || ext_str == "db-shm" { + let db_path = path.with_extension("db"); + if !db_path.exists() && tokio::fs::remove_file(&path).await.is_ok() { + cleaned += 1; + } + } + } + } + } + + cleaned +} + +pub fn routes() -> Router { + Router::new() + .route("/database/status", get(database_status)) + .route("/database/backup", post(create_backup)) + .route("/database/backups", get(list_backups)) + .route("/database/backups/delete", delete(delete_backups)) + .route("/database/restore", post(restore_backup)) } diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 43d43f09c2..f4cf80ae83 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -930,6 +930,169 @@ } } }, + "/database/backup": { + "post": { + "tags": [ + "Database Management" + ], + "operationId": "create_backup", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBackupRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Backup created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBackupResponse" + } + } + } + }, + "401": { + "description": "Unauthorized - Invalid or missing API key" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/database/backups": { + "get": { + "tags": [ + "Database Management" + ], + "operationId": "list_backups", + "responses": { + "200": { + "description": "List of backups retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BackupsListResponse" + } + } + } + }, + "401": { + "description": "Unauthorized - Invalid or missing API key" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/database/backups/delete": { + "delete": { + "tags": [ + "Database Management" + ], + "operationId": "delete_backups", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteBackupsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Backups deleted successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteBackupsResponse" + } + } + } + }, + "400": { + "description": "Bad request - Invalid parameters" + }, + "401": { + "description": "Unauthorized - Invalid or missing API key" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/database/restore": { + "post": { + "tags": [ + "Database Management" + ], + "operationId": "restore_backup", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestoreBackupRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Database restored successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestoreBackupResponse" + } + } + } + }, + "400": { + "description": "Bad request - Invalid backup filename" + }, + "401": { + "description": "Unauthorized - Invalid or missing API key" + }, + "404": { + "description": "Backup not found" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, "/database/status": { "get": { "tags": [ @@ -2184,6 +2347,21 @@ } } }, + "BackupsListResponse": { + "type": "object", + "required": [ + "backups" + ], + "properties": { + "backups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BackupInfo" + }, + "description": "List of available backups" + } + } + }, "ChatRequest": { "type": "object", "required": [ @@ -2315,6 +2493,41 @@ "$ref": "#/components/schemas/Message" } }, + "CreateBackupRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Optional custom name for the backup", + "nullable": true + } + } + }, + "CreateBackupResponse": { + "type": "object", + "required": [ + "filename", + "size", + "createdAt" + ], + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "description": "When the backup was created" + }, + "filename": { + "type": "string", + "description": "Filename of the created backup" + }, + "size": { + "type": "integer", + "format": "int64", + "description": "Size of the backup file in bytes", + "minimum": 0 + } + } + }, "CreateRecipeRequest": { "type": "object", "required": [ @@ -2506,6 +2719,60 @@ } } }, + "DeleteBackupsRequest": { + "type": "object", + "required": [ + "filenames", + "deleteAll", + "cleanupOrphaned" + ], + "properties": { + "cleanupOrphaned": { + "type": "boolean", + "description": "Clean up orphaned WAL/SHM files" + }, + "deleteAll": { + "type": "boolean", + "description": "Delete all backups if true" + }, + "filenames": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of backup filenames to delete" + } + } + }, + "DeleteBackupsResponse": { + "type": "object", + "required": [ + "deleted", + "failed", + "orphanedCleaned" + ], + "properties": { + "deleted": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Successfully deleted files" + }, + "failed": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Files that failed to delete" + }, + "orphanedCleaned": { + "type": "integer", + "description": "Number of orphaned files cleaned", + "minimum": 0 + } + } + }, "DeleteRecipeRequest": { "type": "object", "required": [ @@ -4096,6 +4363,35 @@ } } }, + "RestoreBackupRequest": { + "type": "object", + "required": [ + "filename" + ], + "properties": { + "filename": { + "type": "string", + "description": "Filename of the backup to restore" + } + } + }, + "RestoreBackupResponse": { + "type": "object", + "required": [ + "message", + "restoredFrom" + ], + "properties": { + "message": { + "type": "string", + "description": "Success message" + }, + "restoredFrom": { + "type": "string", + "description": "Backup that was restored" + } + } + }, "ResumeAgentRequest": { "type": "object", "required": [ diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 6f987de487..0c8b2fd843 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DatabaseStatusData, DatabaseStatusErrors, DatabaseStatusResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; +import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateBackupData, CreateBackupErrors, CreateBackupResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DatabaseStatusData, DatabaseStatusErrors, DatabaseStatusResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteBackupsData, DeleteBackupsErrors, DeleteBackupsResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListBackupsData, ListBackupsErrors, ListBackupsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, RestoreBackupData, RestoreBackupErrors, RestoreBackupResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -267,6 +267,46 @@ export const confirmPermission = (options: }); }; +export const createBackup = (options: Options) => { + return (options.client ?? client).post({ + url: '/database/backup', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +export const listBackups = (options?: Options) => { + return (options?.client ?? client).get({ + url: '/database/backups', + ...options + }); +}; + +export const deleteBackups = (options: Options) => { + return (options.client ?? client).delete({ + url: '/database/backups/delete', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +export const restoreBackup = (options: Options) => { + return (options.client ?? client).post({ + url: '/database/restore', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + export const databaseStatus = (options?: Options) => { return (options?.client ?? client).get({ url: '/database/status', diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 84e76c4f8a..e1c9a97b14 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -32,6 +32,13 @@ export type BackupInfo = { size: number; }; +export type BackupsListResponse = { + /** + * List of available backups + */ + backups: Array; +}; + export type ChatRequest = { messages: Array; recipe_name?: string | null; @@ -81,6 +88,28 @@ export type Content = RawTextContent | RawImageContent | RawEmbeddedResource | R export type Conversation = Array; +export type CreateBackupRequest = { + /** + * Optional custom name for the backup + */ + name?: string | null; +}; + +export type CreateBackupResponse = { + /** + * When the backup was created + */ + createdAt: string; + /** + * Filename of the created backup + */ + filename: string; + /** + * Size of the backup file in bytes + */ + size: number; +}; + export type CreateRecipeRequest = { author?: AuthorRequest | null; session_id: string; @@ -133,6 +162,36 @@ export type DecodeRecipeResponse = { recipe: Recipe; }; +export type DeleteBackupsRequest = { + /** + * Clean up orphaned WAL/SHM files + */ + cleanupOrphaned: boolean; + /** + * Delete all backups if true + */ + deleteAll: boolean; + /** + * List of backup filenames to delete + */ + filenames: Array; +}; + +export type DeleteBackupsResponse = { + /** + * Successfully deleted files + */ + deleted: Array; + /** + * Files that failed to delete + */ + failed: Array; + /** + * Number of orphaned files cleaned + */ + orphanedCleaned: number; +}; + export type DeleteRecipeRequest = { id: string; }; @@ -628,6 +687,24 @@ export type Response = { json_schema?: unknown; }; +export type RestoreBackupRequest = { + /** + * Filename of the backup to restore + */ + filename: string; +}; + +export type RestoreBackupResponse = { + /** + * Success message + */ + message: string; + /** + * Backup that was restored + */ + restoredFrom: string; +}; + export type ResumeAgentRequest = { load_model_and_extensions: boolean; session_id: string; @@ -1666,6 +1743,126 @@ export type ConfirmPermissionResponses = { 200: unknown; }; +export type CreateBackupData = { + body: CreateBackupRequest; + path?: never; + query?: never; + url: '/database/backup'; +}; + +export type CreateBackupErrors = { + /** + * Unauthorized - Invalid or missing API key + */ + 401: unknown; + /** + * Internal server error + */ + 500: unknown; +}; + +export type CreateBackupResponses = { + /** + * Backup created successfully + */ + 201: CreateBackupResponse; +}; + +export type CreateBackupResponse2 = CreateBackupResponses[keyof CreateBackupResponses]; + +export type ListBackupsData = { + body?: never; + path?: never; + query?: never; + url: '/database/backups'; +}; + +export type ListBackupsErrors = { + /** + * Unauthorized - Invalid or missing API key + */ + 401: unknown; + /** + * Internal server error + */ + 500: unknown; +}; + +export type ListBackupsResponses = { + /** + * List of backups retrieved successfully + */ + 200: BackupsListResponse; +}; + +export type ListBackupsResponse = ListBackupsResponses[keyof ListBackupsResponses]; + +export type DeleteBackupsData = { + body: DeleteBackupsRequest; + path?: never; + query?: never; + url: '/database/backups/delete'; +}; + +export type DeleteBackupsErrors = { + /** + * Bad request - Invalid parameters + */ + 400: unknown; + /** + * Unauthorized - Invalid or missing API key + */ + 401: unknown; + /** + * Internal server error + */ + 500: unknown; +}; + +export type DeleteBackupsResponses = { + /** + * Backups deleted successfully + */ + 200: DeleteBackupsResponse; +}; + +export type DeleteBackupsResponse2 = DeleteBackupsResponses[keyof DeleteBackupsResponses]; + +export type RestoreBackupData = { + body: RestoreBackupRequest; + path?: never; + query?: never; + url: '/database/restore'; +}; + +export type RestoreBackupErrors = { + /** + * Bad request - Invalid backup filename + */ + 400: unknown; + /** + * Unauthorized - Invalid or missing API key + */ + 401: unknown; + /** + * Backup not found + */ + 404: unknown; + /** + * Internal server error + */ + 500: unknown; +}; + +export type RestoreBackupResponses = { + /** + * Database restored successfully + */ + 200: RestoreBackupResponse; +}; + +export type RestoreBackupResponse2 = RestoreBackupResponses[keyof RestoreBackupResponses]; + export type DatabaseStatusData = { body?: never; path?: never;