improve goose2 agent management flows (#8737)

Signed-off-by: tulsi <tulsi@block.xyz>
This commit is contained in:
tulsi
2026-04-21 17:54:48 -07:00
committed by GitHub
parent 23b3b3dcac
commit 7e2fb3ee5c
46 changed files with 1219 additions and 524 deletions
@@ -1,6 +1,7 @@
use crate::services::personas::PersonaStore;
use crate::types::agents::*;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tauri::State;
#[tauri::command]
@@ -59,6 +60,57 @@ pub fn get_avatars_dir() -> String {
PersonaStore::avatars_dir().to_string_lossy().to_string()
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImportFileReadResult {
pub file_bytes: Vec<u8>,
pub file_name: String,
}
fn validate_import_persona_path(source_path: &str) -> Result<PathBuf, String> {
let path = PathBuf::from(source_path);
if path.as_os_str().is_empty() {
return Err("Selected file path is empty".to_string());
}
let extension = path
.extension()
.and_then(|ext| ext.to_str())
.ok_or_else(|| "Unsupported file type. Expected a .json file.".to_string())?;
if !extension.eq_ignore_ascii_case("json") {
return Err("Unsupported file type. Expected a .json file.".to_string());
}
let metadata = std::fs::metadata(&path)
.map_err(|err| format!("Failed to access import file '{}': {}", path.display(), err))?;
if !metadata.is_file() {
return Err(format!(
"Selected import path '{}' is not a file",
path.display()
));
}
Ok(path)
}
#[tauri::command]
pub fn read_import_persona_file(source_path: String) -> Result<ImportFileReadResult, String> {
let path = validate_import_persona_path(&source_path)?;
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| "Selected file is missing a valid filename".to_string())?
.to_string();
let file_bytes = std::fs::read(&path)
.map_err(|err| format!("Failed to read import file '{}': {}", path.display(), err))?;
Ok(ImportFileReadResult {
file_bytes,
file_name,
})
}
// --- Sprout-compatible persona import/export ---
/// Sprout-compatible persona export format (version 1, camelCase keys).
@@ -208,3 +260,41 @@ pub fn import_personas(
let persona = store.create(request)?;
Ok(vec![persona])
}
#[cfg(test)]
mod tests {
use super::validate_import_persona_path;
#[test]
fn validate_import_persona_path_rejects_non_json_files() {
let path = std::env::temp_dir().join("persona-import.txt");
std::fs::write(&path, b"{}").unwrap();
let result = validate_import_persona_path(path.to_str().unwrap());
assert!(result.is_err());
let _ = std::fs::remove_file(path);
}
#[test]
fn validate_import_persona_path_rejects_directories() {
let dir = std::env::temp_dir().join(format!("persona-import-dir-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let result = validate_import_persona_path(dir.to_str().unwrap());
assert!(result.is_err());
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn validate_import_persona_path_accepts_json_files() {
let path = std::env::temp_dir().join(format!("persona-import-{}.json", std::process::id()));
std::fs::write(&path, b"{}").unwrap();
let validated = validate_import_persona_path(path.to_str().unwrap()).unwrap();
assert_eq!(validated, path);
let _ = std::fs::remove_file(validated);
}
}
+1
View File
@@ -40,6 +40,7 @@ pub fn run() {
commands::agents::refresh_personas,
commands::agents::export_persona,
commands::agents::import_personas,
commands::agents::read_import_persona_file,
commands::agents::save_persona_avatar,
commands::agents::save_persona_avatar_bytes,
commands::agents::get_avatars_dir,
+62 -2
View File
@@ -3,7 +3,7 @@ use crate::types::agents::{
};
use log::warn;
use std::collections::HashSet;
use std::path::PathBuf;
use std::path::{Component, Path, PathBuf};
use std::sync::Mutex;
pub struct PersonaStore {
@@ -197,6 +197,26 @@ impl PersonaStore {
})
}
fn markdown_persona_path(id: &str) -> Result<PathBuf, String> {
let slug = id
.strip_prefix("md-")
.ok_or_else(|| format!("Persona '{}' is not a file-backed persona", id))?;
Self::validate_markdown_persona_slug(slug)?;
Ok(Self::agents_dir().join(format!("{}.md", slug)))
}
fn validate_markdown_persona_slug(slug: &str) -> Result<(), String> {
if slug.chars().any(|c| matches!(c, '/' | '\\')) {
return Err(format!("Persona '{}' has an invalid file-backed ID", slug));
}
let mut components = Path::new(slug).components();
match (components.next(), components.next()) {
(Some(Component::Normal(_)), None) => Ok(()),
_ => Err(format!("Persona '{}' has an invalid file-backed ID", slug)),
}
}
/// Re-scan markdown personas and update the in-memory list.
/// Returns the full updated persona list.
pub fn refresh_markdown(&self) -> Vec<Persona> {
@@ -298,13 +318,29 @@ impl PersonaStore {
let persona = personas
.iter()
.find(|p| p.id == id)
.cloned()
.ok_or_else(|| format!("Persona '{}' not found", id))?;
if persona.is_builtin {
return Err("Cannot delete a built-in persona".to_string());
}
if persona.is_from_disk {
return Err("Cannot delete a markdown persona — delete the file directly".to_string());
let path = Self::markdown_persona_path(id)?;
match std::fs::remove_file(&path) {
Ok(_) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => {
return Err(format!(
"Failed to delete file-backed persona '{}': {}",
path.display(),
err
));
}
}
personas.retain(|p| p.id != id);
self.save_to_disk(&personas);
return Ok(());
}
// Clean up local avatar file if present
@@ -395,3 +431,27 @@ impl PersonaStore {
let _ = std::fs::remove_file(path);
}
}
#[cfg(test)]
mod tests {
use super::PersonaStore;
#[test]
fn markdown_persona_path_rejects_parent_segments() {
assert!(PersonaStore::markdown_persona_path("md-../secret").is_err());
assert!(PersonaStore::markdown_persona_path("md-..").is_err());
}
#[test]
fn markdown_persona_path_rejects_path_separators() {
assert!(PersonaStore::markdown_persona_path("md-nested/slug").is_err());
assert!(PersonaStore::markdown_persona_path(r"md-nested\slug").is_err());
}
#[test]
fn markdown_persona_path_accepts_normal_slug() {
let path = PersonaStore::markdown_persona_path("md-scout").unwrap();
let file_name = path.file_name().and_then(|name| name.to_str());
assert_eq!(file_name, Some("scout.md"));
}
}