diff --git a/crates/manifest/src/paths.rs b/crates/manifest/src/paths.rs index d227ff4c..a8b0cf0b 100644 --- a/crates/manifest/src/paths.rs +++ b/crates/manifest/src/paths.rs @@ -5,6 +5,9 @@ //! - **`config_dir`** — 人が手で書く / 編集する設定。`profiles.toml`, //! `providers.toml`, `models.toml`, `prompts/`, `prompts.toml` 等 //! - **`data_dir`** — プログラムが書く永続データ。`sessions/` 等 +//! - **`secret_data_dir`** — local secret store の読み書き base。既存 +//! secret store は path-derived key を使うため、通常 data とは別に +//! legacy `$HOME/.yoi` を既定にする //! - **`runtime_dir`** — 再起動で消えてよいランタイム状態。socket, //! `workers.json`, `pid` ファイル等 //! @@ -14,6 +17,7 @@ //! |---|---|---|---|---| //! | config | `YOI_CONFIG_DIR` | `$YOI_HOME/config` | `$XDG_CONFIG_HOME/yoi` | `$HOME/.config/yoi` | //! | data | `YOI_DATA_DIR` | `$YOI_HOME` | `$XDG_DATA_HOME/yoi` | `$HOME/.local/share/yoi` | +//! | secrets | `YOI_DATA_DIR` | `$YOI_HOME` | — | `$HOME/.yoi` | //! | runtime | `YOI_RUNTIME_DIR` | `$YOI_HOME/run` | `$XDG_RUNTIME_DIR/yoi` | `$HOME/.yoi/run` | //! //! `YOI_HOME=$X` のとき config は `$X/config`、data は `$X` 直下、 @@ -47,6 +51,19 @@ pub fn data_dir() -> Option { ) } +/// Secret store 用の data directory。 +/// +/// SecretStore は base path から復号 key を導出するため、通常 data dir の +/// XDG fallback には追従させず、明示 override が無い場合は legacy +/// `$HOME/.yoi` を読み書きする。 +pub fn secret_data_dir() -> Option { + resolve_secret_data_dir_from_parts( + env_path("YOI_DATA_DIR"), + env_path("YOI_HOME"), + env_path("HOME"), + ) +} + /// ランタイムディレクトリ。socket, `workers.json`, Worker ごとの `pid` / /// `status.json` 等が置かれる。再起動で消えて構わない。 pub fn runtime_dir() -> Option { @@ -147,6 +164,20 @@ fn resolve_data_dir_from_parts( Some(home?.join(".local").join("share").join("yoi")) } +fn resolve_secret_data_dir_from_parts( + yoi_data_dir: Option, + yoi_home: Option, + home: Option, +) -> Option { + if let Some(p) = yoi_data_dir { + return Some(p); + } + if let Some(p) = yoi_home { + return Some(p); + } + Some(home?.join(".yoi")) +} + fn resolve_runtime_dir_from_parts( yoi_runtime_dir: Option, yoi_home: Option, @@ -322,6 +353,40 @@ mod tests { ); } + #[test] + fn secret_data_dir_falls_back_to_home_dot_yoi() { + assert_eq!( + resolve_secret_data_dir_from_parts(None, None, Some(PathBuf::from("/h"))).unwrap(), + PathBuf::from("/h/.yoi") + ); + } + + #[test] + fn secret_data_dir_yoi_home_wins() { + assert_eq!( + resolve_secret_data_dir_from_parts( + None, + Some(PathBuf::from("/sand")), + Some(PathBuf::from("/h")) + ) + .unwrap(), + PathBuf::from("/sand") + ); + } + + #[test] + fn secret_data_dir_explicit_wins_over_yoi_home() { + assert_eq!( + resolve_secret_data_dir_from_parts( + Some(PathBuf::from("/explicit-data")), + Some(PathBuf::from("/sand")), + Some(PathBuf::from("/h")), + ) + .unwrap(), + PathBuf::from("/explicit-data") + ); + } + #[test] fn runtime_dir_prefers_xdg_runtime_dir() { assert_eq!( diff --git a/crates/tools/src/web.rs b/crates/tools/src/web.rs index 3634db26..b9c829e8 100644 --- a/crates/tools/src/web.rs +++ b/crates/tools/src/web.rs @@ -47,7 +47,7 @@ impl WebTools { .user_agent("yoi-web-tools/0.1") .build() .expect("static reqwest client configuration is valid"); - let secret_store = manifest::paths::data_dir().map(SecretStore::new); + let secret_store = manifest::paths::secret_data_dir().map(SecretStore::new); Self { config, client, diff --git a/crates/tui/src/keys.rs b/crates/tui/src/keys.rs index 277865ef..0b2c69f7 100644 --- a/crates/tui/src/keys.rs +++ b/crates/tui/src/keys.rs @@ -218,10 +218,10 @@ impl std::fmt::Debug for Action { } pub async fn launch() -> ExitCode { - let data_dir = match manifest::paths::data_dir() { + let data_dir = match manifest::paths::secret_data_dir() { Some(path) => path, None => { - eprintln!("yoi keys: could not determine yoi data directory"); + eprintln!("yoi keys: could not determine yoi secret data directory"); return ExitCode::FAILURE; } }; diff --git a/crates/worker/src/model_client.rs b/crates/worker/src/model_client.rs index 6574ec1a..80dffebb 100644 --- a/crates/worker/src/model_client.rs +++ b/crates/worker/src/model_client.rs @@ -70,11 +70,11 @@ struct DefaultSecretResolver; impl SecretResolver for DefaultSecretResolver { fn get_secret(&self, id: &str) -> Result { - let data_dir = manifest::paths::data_dir().ok_or_else(|| secrets::Error::Read { - path: std::path::PathBuf::from(""), + let data_dir = manifest::paths::secret_data_dir().ok_or_else(|| secrets::Error::Read { + path: std::path::PathBuf::from(""), source: std::io::Error::new( std::io::ErrorKind::NotFound, - "could not determine yoi data directory", + "could not determine yoi secret data directory", ), })?; SecretStore::new(data_dir).get(id)