From a4fa1d99315f5f76b2db76a9b13f44e080c30b6e Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Tue, 14 Apr 2026 13:57:11 +1000 Subject: [PATCH] use GOOSE_PATH_ROOT to create isolated app state --- ui/goose2/scripts/run-tauri-dev.sh | 4 +++ ui/goose2/src-tauri/src/commands/acp.rs | 10 ++---- ui/goose2/src-tauri/src/commands/agents.rs | 3 +- ui/goose2/src-tauri/src/commands/projects.rs | 17 ++++----- .../src-tauri/src/services/acp/goose_serve.rs | 11 ++---- .../src-tauri/src/services/goose_paths.rs | 31 ++++++++++++++++ ui/goose2/src-tauri/src/services/mod.rs | 1 + ui/goose2/src-tauri/src/services/personas.rs | 36 +++++-------------- 8 files changed, 59 insertions(+), 54 deletions(-) create mode 100644 ui/goose2/src-tauri/src/services/goose_paths.rs diff --git a/ui/goose2/scripts/run-tauri-dev.sh b/ui/goose2/scripts/run-tauri-dev.sh index 22aa4da5f1..2b08fe6950 100755 --- a/ui/goose2/scripts/run-tauri-dev.sh +++ b/ui/goose2/scripts/run-tauri-dev.sh @@ -11,6 +11,10 @@ case "$mode" in esac profile="${GOOSE2_PROFILE:-}" +if [[ -n "$profile" && -z "${GOOSE_PATH_ROOT:-}" ]]; then + export GOOSE_PATH_ROOT="${TMPDIR:-/tmp}goose2-${profile}" +fi + port_key="$(pwd)::${profile:-default}" vite_port=$( PORT_KEY="$port_key" python3 -c \ diff --git a/ui/goose2/src-tauri/src/commands/acp.rs b/ui/goose2/src-tauri/src/commands/acp.rs index 7b356838cb..1f736ca793 100644 --- a/ui/goose2/src-tauri/src/commands/acp.rs +++ b/ui/goose2/src-tauri/src/commands/acp.rs @@ -7,6 +7,7 @@ use crate::services::acp::{ make_composite_key, search_sessions_via_exports, AcpRunningSession, AcpService, AcpSessionInfo, AcpSessionRegistry, GooseAcpManager, SessionSearchResult, }; +use crate::services::goose_paths::goose_artifacts_dir; const DEPRECATED_PROVIDER_IDS: &[&str] = &["claude-code", "codex", "gemini-cli"]; @@ -22,13 +23,6 @@ fn should_include_provider(provider_id: &str) -> bool { !DEPRECATED_PROVIDER_IDS.contains(&provider_id) } -fn default_artifacts_working_dir() -> PathBuf { - if let Some(home_dir) = dirs::home_dir() { - return home_dir.join(".goose").join("artifacts"); - } - PathBuf::from("/tmp").join(".goose").join("artifacts") -} - fn expand_home_dir(path: PathBuf) -> PathBuf { let path_string = path.to_string_lossy(); @@ -56,7 +50,7 @@ fn resolve_working_dir( .map(|dir| dir.trim().to_string()) .filter(|dir| !dir.is_empty()) .map(PathBuf::from) - .unwrap_or_else(default_artifacts_working_dir); + .unwrap_or_else(goose_artifacts_dir); let working_dir = expand_home_dir(working_dir); let working_dir = if working_dir.is_relative() { diff --git a/ui/goose2/src-tauri/src/commands/agents.rs b/ui/goose2/src-tauri/src/commands/agents.rs index a83499f205..772923239b 100644 --- a/ui/goose2/src-tauri/src/commands/agents.rs +++ b/ui/goose2/src-tauri/src/commands/agents.rs @@ -1,3 +1,4 @@ +use crate::services::goose_paths::goose_avatars_dir; use crate::services::personas::PersonaStore; use crate::types::agents::*; use serde::{Deserialize, Serialize}; @@ -56,7 +57,7 @@ pub fn save_persona_avatar_bytes( /// Returns the absolute path to the avatars directory (~/.goose/avatars/). #[tauri::command] pub fn get_avatars_dir() -> String { - PersonaStore::avatars_dir().to_string_lossy().to_string() + goose_avatars_dir().to_string_lossy().to_string() } // --- Sprout-compatible persona import/export --- diff --git a/ui/goose2/src-tauri/src/commands/projects.rs b/ui/goose2/src-tauri/src/commands/projects.rs index 64ff0ca76b..3a20b4fe3d 100644 --- a/ui/goose2/src-tauri/src/commands/projects.rs +++ b/ui/goose2/src-tauri/src/commands/projects.rs @@ -2,10 +2,7 @@ use serde::Deserialize; use std::fs; use std::path::{Path, PathBuf}; -fn projects_dir() -> Result { - let home = dirs::home_dir().ok_or("Could not determine home directory")?; - Ok(home.join(".goose").join("projects")) -} +use crate::services::goose_paths::goose_projects_dir; fn generate_id() -> String { use std::collections::hash_map::DefaultHasher; @@ -62,7 +59,7 @@ fn slugify(name: &str) -> String { /// Scan all project directories and find the one whose project.json has the given id. /// Returns (dir_path, ProjectInfo). fn find_project_by_id(id: &str) -> Result<(PathBuf, StoredProjectInfo), String> { - let base = projects_dir()?; + let base = goose_projects_dir(); if !base.exists() { return Err(format!("Project with id \"{}\" not found", id)); } @@ -195,7 +192,7 @@ fn project_info_from_stored(project_dir: &Path, stored: StoredProjectInfo) -> Pr #[tauri::command] pub fn list_projects() -> Result, String> { - let dir = projects_dir()?; + let dir = goose_projects_dir(); if !dir.exists() { return Ok(vec![]); @@ -233,7 +230,7 @@ pub fn list_projects() -> Result, String> { #[tauri::command] pub fn list_archived_projects() -> Result, String> { - let dir = projects_dir()?; + let dir = goose_projects_dir(); if !dir.exists() { return Ok(vec![]); } @@ -279,7 +276,7 @@ pub fn create_project( return Err("Project name must not be empty".to_string()); } - let base = projects_dir()?; + let base = goose_projects_dir(); let slug = slugify(&name); // Determine final directory name, avoiding collisions @@ -392,7 +389,7 @@ pub fn get_project(id: String) -> Result { #[tauri::command] pub fn archive_project(id: String) -> Result<(), String> { - let base = projects_dir()?; + let base = goose_projects_dir(); if !base.exists() { return Err("Projects directory not found".to_string()); } @@ -425,7 +422,7 @@ pub fn archive_project(id: String) -> Result<(), String> { #[tauri::command] pub fn restore_project(id: String) -> Result<(), String> { - let base = projects_dir()?; + let base = goose_projects_dir(); if !base.exists() { return Err("Projects directory not found".to_string()); } diff --git a/ui/goose2/src-tauri/src/services/acp/goose_serve.rs b/ui/goose2/src-tauri/src/services/acp/goose_serve.rs index 5603f7cdaa..e270ec833a 100644 --- a/ui/goose2/src-tauri/src/services/acp/goose_serve.rs +++ b/ui/goose2/src-tauri/src/services/acp/goose_serve.rs @@ -6,6 +6,8 @@ use tokio::process::{Child, Command}; use tokio::sync::OnceCell; use tokio_tungstenite::connect_async; +use crate::services::goose_paths::goose_artifacts_dir; + const GOOSE_SERVE_CONNECT_TIMEOUT: Duration = Duration::from_secs(30); const GOOSE_SERVE_CONNECT_RETRY_DELAY: Duration = Duration::from_millis(100); const LOCALHOST: &str = "127.0.0.1"; @@ -61,7 +63,7 @@ impl GooseServeProcess { // Use a stable working directory for the long-lived server process. // Individual sessions will set their own cwd via the ACP protocol. - let working_dir = default_serve_working_dir(); + let working_dir = goose_artifacts_dir(); std::fs::create_dir_all(&working_dir).map_err(|e| { format!( "Failed to create goose serve working directory {}: {e}", @@ -147,13 +149,6 @@ async fn wait_for_server_ready(ws_url: &str, child: &mut Child) -> Result<(), St } } -fn default_serve_working_dir() -> PathBuf { - dirs::home_dir() - .unwrap_or_else(|| PathBuf::from("/tmp")) - .join(".goose") - .join("artifacts") -} - // --------------------------------------------------------------------------- // Binary resolution // --------------------------------------------------------------------------- diff --git a/ui/goose2/src-tauri/src/services/goose_paths.rs b/ui/goose2/src-tauri/src/services/goose_paths.rs new file mode 100644 index 0000000000..0672be1f1e --- /dev/null +++ b/ui/goose2/src-tauri/src/services/goose_paths.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; + +pub fn goose_home_dir() -> PathBuf { + if let Ok(root) = std::env::var("GOOSE_PATH_ROOT") { + return PathBuf::from(root); + } + + dirs::home_dir() + .unwrap_or_else(|| PathBuf::from("/tmp")) + .join(".goose") +} + +pub fn goose_artifacts_dir() -> PathBuf { + goose_home_dir().join("artifacts") +} + +pub fn goose_projects_dir() -> PathBuf { + goose_home_dir().join("projects") +} + +pub fn goose_personas_path() -> PathBuf { + goose_home_dir().join("personas.json") +} + +pub fn goose_agents_dir() -> PathBuf { + goose_home_dir().join("agents") +} + +pub fn goose_avatars_dir() -> PathBuf { + goose_home_dir().join("avatars") +} diff --git a/ui/goose2/src-tauri/src/services/mod.rs b/ui/goose2/src-tauri/src/services/mod.rs index 77759b3ac4..4eaebcc577 100644 --- a/ui/goose2/src-tauri/src/services/mod.rs +++ b/ui/goose2/src-tauri/src/services/mod.rs @@ -1,4 +1,5 @@ pub mod acp; pub mod goose_config; +pub mod goose_paths; pub mod personas; pub(crate) mod provider_defs; diff --git a/ui/goose2/src-tauri/src/services/personas.rs b/ui/goose2/src-tauri/src/services/personas.rs index 8a3f1c4c72..cc9fd8107d 100644 --- a/ui/goose2/src-tauri/src/services/personas.rs +++ b/ui/goose2/src-tauri/src/services/personas.rs @@ -1,6 +1,9 @@ use crate::types::agents::{ builtin_personas, Avatar, CreatePersonaRequest, Persona, UpdatePersonaRequest, }; +use crate::services::goose_paths::{ + goose_agents_dir, goose_avatars_dir, goose_personas_path, +}; use log::warn; use std::collections::HashSet; use std::path::PathBuf; @@ -20,7 +23,7 @@ struct MarkdownFrontmatter { impl PersonaStore { pub fn new() -> Self { - let store_path = Self::store_path(); + let store_path = goose_personas_path(); let stored = Self::load_from_disk(&store_path); let markdown = Self::load_markdown_personas(); let merged = Self::merge_all(stored, markdown); @@ -30,19 +33,6 @@ impl PersonaStore { } } - fn store_path() -> PathBuf { - let base = dirs::home_dir().expect("home dir"); - base.join(".goose").join("personas.json") - } - - /// Path to the avatars directory (~/.goose/avatars/). - pub fn avatars_dir() -> PathBuf { - dirs::home_dir() - .expect("home dir") - .join(".goose") - .join("avatars") - } - fn load_from_disk(path: &PathBuf) -> Vec { match std::fs::read_to_string(path) { Ok(contents) => serde_json::from_str(&contents).unwrap_or_default(), @@ -86,17 +76,9 @@ impl PersonaStore { result } - /// Directory containing markdown persona files. - fn agents_dir() -> PathBuf { - dirs::home_dir() - .expect("home dir") - .join(".goose") - .join("agents") - } - /// Scan `~/.goose/agents/*.md` and parse each into a Persona. fn load_markdown_personas() -> Vec { - let dir = Self::agents_dir(); + let dir = goose_agents_dir(); if !dir.is_dir() { return Vec::new(); } @@ -309,7 +291,7 @@ impl PersonaStore { // Clean up local avatar file if present if let Some(Avatar::Local(filename)) = &persona.avatar { - let path = Self::avatars_dir().join(filename); + let path = goose_avatars_dir().join(filename); let _ = std::fs::remove_file(path); } @@ -321,7 +303,7 @@ impl PersonaStore { /// Copy an avatar image from a source path to ~/.goose/avatars/{persona_id}.{ext}. /// Returns the filename (not full path). pub fn save_avatar_from_path(persona_id: &str, source_path: &str) -> Result { - let avatars_dir = Self::avatars_dir(); + let avatars_dir = goose_avatars_dir(); std::fs::create_dir_all(&avatars_dir) .map_err(|e| format!("Failed to create avatars directory: {}", e))?; @@ -362,7 +344,7 @@ impl PersonaStore { bytes: &[u8], extension: &str, ) -> Result { - let avatars_dir = Self::avatars_dir(); + let avatars_dir = goose_avatars_dir(); std::fs::create_dir_all(&avatars_dir) .map_err(|e| format!("Failed to create avatars directory: {}", e))?; @@ -391,7 +373,7 @@ impl PersonaStore { /// Delete avatar file for a persona. #[allow(dead_code)] pub fn delete_avatar_file(filename: &str) { - let path = Self::avatars_dir().join(filename); + let path = goose_avatars_dir().join(filename); let _ = std::fs::remove_file(path); } }