home-dirの整理
This commit is contained in:
parent
9998539e71
commit
f8fe6f83aa
1
TODO.md
1
TODO.md
|
|
@ -19,4 +19,5 @@
|
|||
- [ ] Phase 2 consolidation → [tickets/memory-phase2-consolidation.md](tickets/memory-phase2-consolidation.md)
|
||||
- [ ] 使用頻度メトリクス + Knowledge 化候補レポート → [tickets/memory-usage-metrics.md](tickets/memory-usage-metrics.md)
|
||||
- [ ] GC(定期再評価) → [tickets/memory-gc.md](tickets/memory-gc.md)
|
||||
- [ ] LLM 生成設定の manifest 露出整理 → [tickets/worker-generation-settings.md](tickets/worker-generation-settings.md)
|
||||
- [ ] モデル reasoning/thinking 制御の内部抽象整理 → [tickets/model-reasoning-control.md](tickets/model-reasoning-control.md)
|
||||
|
|
|
|||
|
|
@ -3,16 +3,13 @@
|
|||
//! Pod manifests are assembled from up to three on-disk layers (see
|
||||
//! `pod::PodFactory` for the full cascade story):
|
||||
//!
|
||||
//! 1. **User manifest** at `$XDG_CONFIG_HOME/insomnia/manifest.toml`,
|
||||
//! falling back to `$HOME/.config/insomnia/manifest.toml`
|
||||
//! 1. **User manifest** — see [`crate::paths::user_manifest_path`]
|
||||
//! 2. **Project manifest** at the closest `.insomnia/manifest.toml`
|
||||
//! found by walking up from a starting directory (typically `cwd`)
|
||||
//! 3. **Programmatic overlay** supplied at the call site
|
||||
//!
|
||||
//! This module owns the conventions for (1) and (2): where each file
|
||||
//! lives and how to parse it. Callers (pod's factory, the TUI's spawn
|
||||
//! UI, future GUI flows) all share these helpers so the conventions
|
||||
//! live in one place.
|
||||
//! This module owns the project-layer discovery and the parser glue.
|
||||
//! User-layer path resolution lives in [`crate::paths`].
|
||||
//!
|
||||
//! Cascade *merging* and final validation stay outside this module —
|
||||
//! that's the data layer's responsibility (`PodManifestConfig::merge`
|
||||
|
|
@ -40,28 +37,6 @@ pub enum LayerLoadError {
|
|||
},
|
||||
}
|
||||
|
||||
/// Conventional path of the user manifest:
|
||||
/// `$XDG_CONFIG_HOME/insomnia/manifest.toml`, falling back to
|
||||
/// `$HOME/.config/insomnia/manifest.toml`. Returns `None` when neither
|
||||
/// env var is set to a non-empty value.
|
||||
///
|
||||
/// Existence of the file is **not** checked here — callers decide
|
||||
/// whether a missing file is an error or a silent skip.
|
||||
pub fn user_manifest_path() -> Option<PathBuf> {
|
||||
if let Ok(dir) = std::env::var("XDG_CONFIG_HOME") {
|
||||
if !dir.is_empty() {
|
||||
return Some(PathBuf::from(dir).join("insomnia").join("manifest.toml"));
|
||||
}
|
||||
}
|
||||
let home = std::env::var("HOME").ok().filter(|s| !s.is_empty())?;
|
||||
Some(
|
||||
PathBuf::from(home)
|
||||
.join(".config")
|
||||
.join("insomnia")
|
||||
.join("manifest.toml"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Walk up from `start` looking for `.insomnia/manifest.toml`. Returns
|
||||
/// the closest match, or `None` if none is found before reaching the
|
||||
/// filesystem root.
|
||||
|
|
@ -147,29 +122,4 @@ name = "from-disk"
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_manifest_path_uses_xdg_when_set() {
|
||||
let saved_xdg = std::env::var("XDG_CONFIG_HOME").ok();
|
||||
let saved_home = std::env::var("HOME").ok();
|
||||
// SAFETY: tests in this module are not run concurrently with
|
||||
// env-mutating threads (cargo's default test harness already
|
||||
// serialises tests inside one binary, and these helpers don't
|
||||
// spawn threads of their own).
|
||||
unsafe {
|
||||
std::env::set_var("XDG_CONFIG_HOME", "/tmp/xdg-conf");
|
||||
std::env::remove_var("HOME");
|
||||
}
|
||||
let p = user_manifest_path().unwrap();
|
||||
assert_eq!(p, PathBuf::from("/tmp/xdg-conf/insomnia/manifest.toml"));
|
||||
unsafe {
|
||||
match saved_xdg {
|
||||
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
|
||||
None => std::env::remove_var("XDG_CONFIG_HOME"),
|
||||
}
|
||||
match saved_home {
|
||||
Some(v) => std::env::set_var("HOME", v),
|
||||
None => std::env::remove_var("HOME"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ mod cascade;
|
|||
mod config;
|
||||
pub mod defaults;
|
||||
mod model;
|
||||
pub mod paths;
|
||||
mod scope;
|
||||
|
||||
pub use cascade::{
|
||||
LayerLoadError, find_project_manifest_from, load_layer, user_manifest_path,
|
||||
};
|
||||
pub use cascade::{LayerLoadError, find_project_manifest_from, load_layer};
|
||||
pub use paths::user_manifest_path;
|
||||
pub use config::{
|
||||
CompactionConfigPartial, PodManifestConfig, PodMetaConfig, ResolveError,
|
||||
ToolOutputLimitsPartial, WorkerManifestConfig,
|
||||
|
|
|
|||
330
crates/manifest/src/paths.rs
Normal file
330
crates/manifest/src/paths.rs
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
//! Insomnia のホームディレクトリ配下のパス解決を一元化するモジュール。
|
||||
//!
|
||||
//! 用途別に三つの base directory を持つ:
|
||||
//!
|
||||
//! - **`config_dir`** — 人が手で書く / 編集する設定。`manifest.toml`,
|
||||
//! `providers.toml`, `models.toml`, `prompts/`, `prompts.toml` 等
|
||||
//! - **`data_dir`** — プログラムが書く永続データ。`sessions/` 等
|
||||
//! - **`runtime_dir`** — 再起動で消えてよいランタイム状態。socket,
|
||||
//! `scope.lock`, `pid` ファイル等
|
||||
//!
|
||||
//! ## 解決順 (優先順位高 → 低)
|
||||
//!
|
||||
//! | base | 1. `INSOMNIA_<KIND>_DIR` | 2. `INSOMNIA_HOME` | 3. `XDG_*` | 4. 既定 |
|
||||
//! |---|---|---|---|---|
|
||||
//! | config | `INSOMNIA_CONFIG_DIR` | `$INSOMNIA_HOME/config` | `$XDG_CONFIG_HOME/insomnia` | `$HOME/.config/insomnia` |
|
||||
//! | data | `INSOMNIA_DATA_DIR` | `$INSOMNIA_HOME` | — | `$HOME/.insomnia` |
|
||||
//! | runtime | `INSOMNIA_RUNTIME_DIR` | `$INSOMNIA_HOME/run` | `$XDG_RUNTIME_DIR/insomnia` | `$HOME/.insomnia/run` |
|
||||
//!
|
||||
//! `INSOMNIA_HOME=$X` のとき config は `$X/config`、data は `$X` 直下、
|
||||
//! runtime は `$X/run` に集約される。テストや sandbox 利用ではこれ一本
|
||||
//! で全部 tempdir に向けられる。
|
||||
//!
|
||||
//! 解決された各 base が存在するか / ディレクトリかは保証しない —
|
||||
//! 呼び出し側がファイル操作の前に作成 / 検査する。
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// 設定ディレクトリ。`manifest.toml`, `providers.toml`, `models.toml`,
|
||||
/// `prompts/` などが置かれる。
|
||||
pub fn config_dir() -> Option<PathBuf> {
|
||||
if let Some(p) = env_path("INSOMNIA_CONFIG_DIR") {
|
||||
return Some(p);
|
||||
}
|
||||
if let Some(p) = env_path("INSOMNIA_HOME") {
|
||||
return Some(p.join("config"));
|
||||
}
|
||||
if let Some(p) = env_path("XDG_CONFIG_HOME") {
|
||||
return Some(p.join("insomnia"));
|
||||
}
|
||||
Some(env_path("HOME")?.join(".config").join("insomnia"))
|
||||
}
|
||||
|
||||
/// データディレクトリ。`sessions/` などプログラムが書く永続データの
|
||||
/// 置き場。
|
||||
pub fn data_dir() -> Option<PathBuf> {
|
||||
if let Some(p) = env_path("INSOMNIA_DATA_DIR") {
|
||||
return Some(p);
|
||||
}
|
||||
if let Some(p) = env_path("INSOMNIA_HOME") {
|
||||
return Some(p);
|
||||
}
|
||||
Some(env_path("HOME")?.join(".insomnia"))
|
||||
}
|
||||
|
||||
/// ランタイムディレクトリ。socket, `scope.lock`, Pod ごとの `pid` /
|
||||
/// `status.json` 等が置かれる。再起動で消えて構わない。
|
||||
pub fn runtime_dir() -> Option<PathBuf> {
|
||||
if let Some(p) = env_path("INSOMNIA_RUNTIME_DIR") {
|
||||
return Some(p);
|
||||
}
|
||||
if let Some(p) = env_path("INSOMNIA_HOME") {
|
||||
return Some(p.join("run"));
|
||||
}
|
||||
if let Some(p) = env_path("XDG_RUNTIME_DIR") {
|
||||
return Some(p.join("insomnia"));
|
||||
}
|
||||
Some(env_path("HOME")?.join(".insomnia").join("run"))
|
||||
}
|
||||
|
||||
// ---- well-known file getters ------------------------------------------------
|
||||
|
||||
/// `<config_dir>/manifest.toml` — user manifest。
|
||||
pub fn user_manifest_path() -> Option<PathBuf> {
|
||||
Some(config_dir()?.join("manifest.toml"))
|
||||
}
|
||||
|
||||
/// `<config_dir>/prompts/` — user prompts ライブラリ。
|
||||
pub fn user_prompts_dir() -> Option<PathBuf> {
|
||||
Some(config_dir()?.join("prompts"))
|
||||
}
|
||||
|
||||
/// `<config_dir>/prompts.toml` — user prompt pack。
|
||||
pub fn user_pack_file() -> Option<PathBuf> {
|
||||
Some(config_dir()?.join("prompts.toml"))
|
||||
}
|
||||
|
||||
/// `<config_dir>/<file_name>` — providers.toml / models.toml 等の
|
||||
/// user override ファイル。
|
||||
pub fn user_catalog_override(file_name: &str) -> Option<PathBuf> {
|
||||
Some(config_dir()?.join(file_name))
|
||||
}
|
||||
|
||||
/// `<data_dir>/sessions/` — session store のデフォルト位置。
|
||||
pub fn sessions_dir() -> Option<PathBuf> {
|
||||
Some(data_dir()?.join("sessions"))
|
||||
}
|
||||
|
||||
/// `<runtime_dir>/scope.lock` — machine-wide scope allocation registry。
|
||||
pub fn scope_lock_path() -> Option<PathBuf> {
|
||||
Some(runtime_dir()?.join("scope.lock"))
|
||||
}
|
||||
|
||||
/// `<runtime_dir>/<pod_name>/` — Pod ごとのランタイムディレクトリ。
|
||||
pub fn pod_runtime_dir(pod_name: &str) -> Option<PathBuf> {
|
||||
Some(runtime_dir()?.join(pod_name))
|
||||
}
|
||||
|
||||
/// `<runtime_dir>/<pod_name>/sock` — Pod の Unix socket パス (TUI が
|
||||
/// attach 時に使う)。Pod プロセスが実際に socket を作成するのは
|
||||
/// `RuntimeDir::socket_path()` 経由だが、外部からの予測はこの関数で
|
||||
/// 行う。
|
||||
pub fn pod_socket_path(pod_name: &str) -> Option<PathBuf> {
|
||||
Some(pod_runtime_dir(pod_name)?.join("sock"))
|
||||
}
|
||||
|
||||
// ---- internals --------------------------------------------------------------
|
||||
|
||||
/// 空文字列の env は未設定として扱う。`std::env::var` は `Ok("")` と
|
||||
/// `Err(NotPresent)` を区別するが、パス解決においては両者を未設定と
|
||||
/// 同等に扱うのが直感的。
|
||||
fn env_path(name: &str) -> Option<PathBuf> {
|
||||
std::env::var(name)
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(PathBuf::from)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::{Mutex, MutexGuard, OnceLock};
|
||||
|
||||
/// プロセス全体で env を弄るテスト同士が並行に走らないように保護
|
||||
/// する。Cargo の test harness はファイル単位で別プロセスにせず
|
||||
/// マルチスレッドで実行するため、env を読む全テストはこの lock を
|
||||
/// 取ってから操作する。
|
||||
fn env_lock() -> MutexGuard<'static, ()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| Mutex::new(()))
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// テスト中だけ env を上書きし、drop 時に元の値に戻す RAII guard。
|
||||
struct EnvGuard {
|
||||
vars: Vec<(&'static str, Option<String>)>,
|
||||
_lock: MutexGuard<'static, ()>,
|
||||
}
|
||||
|
||||
impl EnvGuard {
|
||||
fn new(overrides: &[(&'static str, Option<&str>)]) -> Self {
|
||||
let lock = env_lock();
|
||||
let names = [
|
||||
"INSOMNIA_CONFIG_DIR",
|
||||
"INSOMNIA_DATA_DIR",
|
||||
"INSOMNIA_RUNTIME_DIR",
|
||||
"INSOMNIA_HOME",
|
||||
"XDG_CONFIG_HOME",
|
||||
"XDG_RUNTIME_DIR",
|
||||
"HOME",
|
||||
];
|
||||
let saved: Vec<_> = names
|
||||
.iter()
|
||||
.map(|n| (*n, std::env::var(n).ok()))
|
||||
.collect();
|
||||
// SAFETY: env_lock() 取得済みなので env への並行アクセスは
|
||||
// この test バイナリ内では発生しない。
|
||||
unsafe {
|
||||
for (n, _) in &saved {
|
||||
std::env::remove_var(n);
|
||||
}
|
||||
for (n, v) in overrides {
|
||||
if let Some(v) = v {
|
||||
std::env::set_var(n, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
Self {
|
||||
vars: saved,
|
||||
_lock: lock,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvGuard {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: lock を握ったまま元に戻す。
|
||||
unsafe {
|
||||
for (n, v) in &self.vars {
|
||||
match v {
|
||||
Some(v) => std::env::set_var(n, v),
|
||||
None => std::env::remove_var(n),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_dir_falls_back_to_home_dot_config() {
|
||||
let _g = EnvGuard::new(&[("HOME", Some("/h"))]);
|
||||
assert_eq!(config_dir().unwrap(), PathBuf::from("/h/.config/insomnia"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_dir_uses_xdg_when_set() {
|
||||
let _g = EnvGuard::new(&[
|
||||
("HOME", Some("/h")),
|
||||
("XDG_CONFIG_HOME", Some("/x")),
|
||||
]);
|
||||
assert_eq!(config_dir().unwrap(), PathBuf::from("/x/insomnia"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_dir_insomnia_home_outranks_xdg() {
|
||||
let _g = EnvGuard::new(&[
|
||||
("HOME", Some("/h")),
|
||||
("XDG_CONFIG_HOME", Some("/x")),
|
||||
("INSOMNIA_HOME", Some("/sand")),
|
||||
]);
|
||||
assert_eq!(config_dir().unwrap(), PathBuf::from("/sand/config"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_dir_explicit_wins_over_insomnia_home() {
|
||||
let _g = EnvGuard::new(&[
|
||||
("HOME", Some("/h")),
|
||||
("INSOMNIA_HOME", Some("/sand")),
|
||||
("INSOMNIA_CONFIG_DIR", Some("/explicit-cfg")),
|
||||
]);
|
||||
assert_eq!(config_dir().unwrap(), PathBuf::from("/explicit-cfg"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_dir_default_is_dot_insomnia() {
|
||||
let _g = EnvGuard::new(&[("HOME", Some("/h"))]);
|
||||
assert_eq!(data_dir().unwrap(), PathBuf::from("/h/.insomnia"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_dir_insomnia_home_is_data_dir_itself() {
|
||||
let _g = EnvGuard::new(&[
|
||||
("HOME", Some("/h")),
|
||||
("INSOMNIA_HOME", Some("/sand")),
|
||||
]);
|
||||
assert_eq!(data_dir().unwrap(), PathBuf::from("/sand"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_dir_prefers_xdg_runtime_dir() {
|
||||
let _g = EnvGuard::new(&[
|
||||
("HOME", Some("/h")),
|
||||
("XDG_RUNTIME_DIR", Some("/run/user/1000")),
|
||||
]);
|
||||
assert_eq!(
|
||||
runtime_dir().unwrap(),
|
||||
PathBuf::from("<runtime-dir>")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_dir_falls_back_to_dot_insomnia_run() {
|
||||
let _g = EnvGuard::new(&[("HOME", Some("/h"))]);
|
||||
assert_eq!(runtime_dir().unwrap(), PathBuf::from("/h/.insomnia/run"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_dir_insomnia_home_is_run_subdir() {
|
||||
let _g = EnvGuard::new(&[
|
||||
("HOME", Some("/h")),
|
||||
("XDG_RUNTIME_DIR", Some("/run/user/1000")),
|
||||
("INSOMNIA_HOME", Some("/sand")),
|
||||
]);
|
||||
assert_eq!(runtime_dir().unwrap(), PathBuf::from("/sand/run"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_env_treated_as_unset() {
|
||||
let _g = EnvGuard::new(&[
|
||||
("HOME", Some("/h")),
|
||||
("XDG_CONFIG_HOME", Some("")),
|
||||
]);
|
||||
assert_eq!(config_dir().unwrap(), PathBuf::from("/h/.config/insomnia"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_none_when_nothing_set() {
|
||||
let _g = EnvGuard::new(&[]);
|
||||
assert!(config_dir().is_none());
|
||||
assert!(data_dir().is_none());
|
||||
assert!(runtime_dir().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn well_known_files_compose_off_base_dirs() {
|
||||
let _g = EnvGuard::new(&[("INSOMNIA_HOME", Some("/sand"))]);
|
||||
assert_eq!(
|
||||
user_manifest_path().unwrap(),
|
||||
PathBuf::from("/sand/config/manifest.toml")
|
||||
);
|
||||
assert_eq!(
|
||||
user_prompts_dir().unwrap(),
|
||||
PathBuf::from("/sand/config/prompts")
|
||||
);
|
||||
assert_eq!(
|
||||
user_pack_file().unwrap(),
|
||||
PathBuf::from("/sand/config/prompts.toml")
|
||||
);
|
||||
assert_eq!(
|
||||
user_catalog_override("providers.toml").unwrap(),
|
||||
PathBuf::from("/sand/config/providers.toml")
|
||||
);
|
||||
assert_eq!(
|
||||
sessions_dir().unwrap(),
|
||||
PathBuf::from("/sand/sessions")
|
||||
);
|
||||
assert_eq!(
|
||||
scope_lock_path().unwrap(),
|
||||
PathBuf::from("/sand/run/scope.lock")
|
||||
);
|
||||
assert_eq!(
|
||||
pod_runtime_dir("foo").unwrap(),
|
||||
PathBuf::from("/sand/run/foo")
|
||||
);
|
||||
assert_eq!(
|
||||
pod_socket_path("foo").unwrap(),
|
||||
PathBuf::from("/sand/run/foo/sock")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@
|
|||
//! the user or overlay layers lay out their own paths:
|
||||
//!
|
||||
//! - user manifest: base = the directory holding the manifest file
|
||||
//! (which is `manifest::paths::config_dir()` when loaded via the
|
||||
//! `_auto` variant)
|
||||
//! - project manifest: base = the **project root** (the parent of
|
||||
//! `.insomnia/`, not `.insomnia/` itself) so that natural project
|
||||
//! manifests with `target = "."` cover the whole workspace
|
||||
|
|
@ -29,7 +31,7 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
use manifest::{
|
||||
LayerLoadError, PodManifest, PodManifestConfig, ResolveError, find_project_manifest_from,
|
||||
load_layer, user_manifest_path,
|
||||
load_layer, paths,
|
||||
};
|
||||
|
||||
use crate::prompt::loader::PromptLoader;
|
||||
|
|
@ -101,21 +103,19 @@ impl PodFactory {
|
|||
Self::default()
|
||||
}
|
||||
|
||||
/// Attempt to load the user manifest from the XDG config directory.
|
||||
///
|
||||
/// Looks at `$XDG_CONFIG_HOME/insomnia/manifest.toml` first, then
|
||||
/// falls back to `$HOME/.config/insomnia/manifest.toml`. If neither
|
||||
/// env var is set, or the resolved file does not exist, the call
|
||||
/// is a no-op — user manifests are optional.
|
||||
/// Attempt to load the user manifest from the user's config
|
||||
/// directory (see [`manifest::paths::config_dir`] for how the path
|
||||
/// is resolved). If the resolved file does not exist, the call is a
|
||||
/// no-op — user manifests are optional.
|
||||
pub fn with_user_manifest_auto(mut self) -> Result<Self, FactoryError> {
|
||||
let Some(path) = user_manifest_path() else {
|
||||
let Some(path) = paths::user_manifest_path() else {
|
||||
return Ok(self);
|
||||
};
|
||||
if path.exists() {
|
||||
let base = manifest_base(&path)?;
|
||||
self.user = Some((load_layer(&path)?, base.clone()));
|
||||
self.user_prompts_dir = Some(base.join("prompts"));
|
||||
self.user_pack_file = Some(base.join("prompts.toml"));
|
||||
self.user_prompts_dir = paths::user_prompts_dir();
|
||||
self.user_pack_file = paths::user_pack_file();
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use std::path::PathBuf;
|
|||
use std::process::ExitCode;
|
||||
|
||||
use clap::Parser;
|
||||
use manifest::paths;
|
||||
use pod::{Pod, PodController, PodFactory};
|
||||
use session_store::FsStore;
|
||||
|
||||
|
|
@ -11,8 +12,8 @@ use session_store::FsStore;
|
|||
about = "Spawn a Pod process from cascaded manifest layers"
|
||||
)]
|
||||
struct Cli {
|
||||
/// User manifest TOML. Defaults to
|
||||
/// `$XDG_CONFIG_HOME/insomnia/manifest.toml`.
|
||||
/// User manifest TOML. Defaults to `<config_dir>/manifest.toml`
|
||||
/// (see `manifest::paths`).
|
||||
#[arg(long, value_name = "PATH")]
|
||||
user_manifest: Option<PathBuf>,
|
||||
|
||||
|
|
@ -28,7 +29,7 @@ struct Cli {
|
|||
overlay: Option<String>,
|
||||
|
||||
/// Directory for session persistence. Defaults to
|
||||
/// `~/.insomnia/sessions/`.
|
||||
/// `<data_dir>/sessions/` (see `manifest::paths`).
|
||||
#[arg(short, long)]
|
||||
store: Option<PathBuf>,
|
||||
|
||||
|
|
@ -44,25 +45,6 @@ struct Cli {
|
|||
callback: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn default_store_dir() -> Result<PathBuf, std::io::Error> {
|
||||
let home = std::env::var("HOME")
|
||||
.map_err(|_| std::io::Error::new(std::io::ErrorKind::NotFound, "HOME is not set"))?;
|
||||
Ok(PathBuf::from(home).join(".insomnia").join("sessions"))
|
||||
}
|
||||
|
||||
fn default_runtime_dir() -> Result<PathBuf, std::io::Error> {
|
||||
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
|
||||
Ok(PathBuf::from(runtime_dir).join("insomnia"))
|
||||
} else if let Ok(home) = std::env::var("HOME") {
|
||||
Ok(PathBuf::from(home).join(".insomnia").join("run"))
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"neither XDG_RUNTIME_DIR nor HOME is set",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_factory(cli: &Cli) -> Result<PodFactory, String> {
|
||||
let mut factory = PodFactory::new();
|
||||
|
||||
|
|
@ -115,7 +97,7 @@ async fn main() -> ExitCode {
|
|||
|
||||
// Initialize persistent store
|
||||
let store_dir = cli.store.clone().unwrap_or_else(|| {
|
||||
default_store_dir().unwrap_or_else(|_| PathBuf::from(".insomnia/sessions"))
|
||||
paths::sessions_dir().unwrap_or_else(|| PathBuf::from(".insomnia/sessions"))
|
||||
});
|
||||
let store = match FsStore::new(&store_dir).await {
|
||||
Ok(s) => s,
|
||||
|
|
@ -152,10 +134,13 @@ async fn main() -> ExitCode {
|
|||
let pod_name = pod.manifest().pod.name.clone();
|
||||
|
||||
// Spawn the controller (starts socket server)
|
||||
let runtime_base = match default_runtime_dir() {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
eprintln!("error: {e}");
|
||||
let runtime_base = match paths::runtime_dir() {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
eprintln!(
|
||||
"error: could not resolve runtime directory \
|
||||
(set INSOMNIA_HOME, INSOMNIA_RUNTIME_DIR, XDG_RUNTIME_DIR, or HOME)"
|
||||
);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use manifest::ScopeRule;
|
||||
use manifest::{ScopeRule, paths};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ pub struct SpawnedPodRecord {
|
|||
/// Manages the Pod's runtime directory on tmpfs.
|
||||
///
|
||||
/// ```text
|
||||
/// $XDG_RUNTIME_DIR/insomnia/{pod_name}/
|
||||
/// <runtime_dir>/{pod_name}/
|
||||
/// ├── pid
|
||||
/// ├── status.json
|
||||
/// ├── manifest.toml
|
||||
|
|
@ -36,6 +36,7 @@ pub struct SpawnedPodRecord {
|
|||
/// └── sock (created by socket listener, not by RuntimeDir)
|
||||
/// ```
|
||||
///
|
||||
/// `<runtime_dir>` is resolved via [`manifest::paths::runtime_dir`].
|
||||
/// Files are written atomically (write tmp → rename).
|
||||
/// The directory is removed on drop.
|
||||
pub struct RuntimeDir {
|
||||
|
|
@ -54,10 +55,8 @@ impl RuntimeDir {
|
|||
Ok(Self { path })
|
||||
}
|
||||
|
||||
/// Create in the default base directory.
|
||||
///
|
||||
/// Uses `$XDG_RUNTIME_DIR/insomnia/` if available,
|
||||
/// otherwise falls back to `~/.insomnia/run/`.
|
||||
/// Create in the default base directory resolved via
|
||||
/// [`manifest::paths::runtime_dir`].
|
||||
pub async fn create_default(pod_name: &str) -> Result<Self, io::Error> {
|
||||
let base = default_base()?;
|
||||
Self::create(&base, pod_name).await
|
||||
|
|
@ -118,20 +117,16 @@ async fn atomic_write(target: &Path, content: &[u8]) -> Result<(), io::Error> {
|
|||
|
||||
/// Resolve the default base directory for runtime data.
|
||||
///
|
||||
/// Public so the scope-lock registry (which lives outside the
|
||||
/// `RuntimeDir` instance lifecycle) can predict a Pod's socket path
|
||||
/// without constructing a `RuntimeDir` first.
|
||||
/// Thin wrapper over [`manifest::paths::runtime_dir`] that converts a
|
||||
/// missing-env situation into an `io::Error`.
|
||||
pub fn default_base() -> Result<PathBuf, io::Error> {
|
||||
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
|
||||
Ok(PathBuf::from(runtime_dir).join("insomnia"))
|
||||
} else if let Ok(home) = std::env::var("HOME") {
|
||||
Ok(PathBuf::from(home).join(".insomnia").join("run"))
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
paths::runtime_dir().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"neither XDG_RUNTIME_DIR nor HOME is set",
|
||||
))
|
||||
}
|
||||
"could not resolve runtime directory (no INSOMNIA_HOME / \
|
||||
INSOMNIA_RUNTIME_DIR / XDG_RUNTIME_DIR / HOME)",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
//! Machine-wide scope allocation registry.
|
||||
//!
|
||||
//! A single JSON file at `$XDG_RUNTIME_DIR/insomnia/scope.lock` records
|
||||
//! every live Pod's scope allocation. File-level `flock(2)` serialises
|
||||
//! access across processes so spawn sequences from unrelated Pods can't
|
||||
//! race.
|
||||
//! A single JSON file at `<runtime_dir>/scope.lock` records every live
|
||||
//! Pod's scope allocation (see [`manifest::paths::scope_lock_path`] for
|
||||
//! how the path is resolved). File-level `flock(2)` serialises access
|
||||
//! across processes so spawn sequences from unrelated Pods can't race.
|
||||
//!
|
||||
//! Each Pod, when starting, acquires the lock, reclaims stale entries
|
||||
//! (Pods whose PID has died), checks that its requested write scope
|
||||
|
|
@ -19,7 +19,7 @@ use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt};
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use fs4::fs_std::FileExt;
|
||||
use manifest::{Permission, ScopeRule};
|
||||
use manifest::{Permission, ScopeRule, paths};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// On-disk representation of the allocation table.
|
||||
|
|
@ -62,27 +62,18 @@ impl LockFile {
|
|||
}
|
||||
}
|
||||
|
||||
/// Default on-disk path: `$XDG_RUNTIME_DIR/insomnia/scope.lock`,
|
||||
/// falling back to `~/.insomnia/run/scope.lock` when XDG is unset.
|
||||
///
|
||||
/// Honours `INSOMNIA_SCOPE_LOCK` as an explicit override, primarily so
|
||||
/// tests can point at a tempdir without polluting the user's runtime
|
||||
/// directory.
|
||||
/// Default on-disk path: `<runtime_dir>/scope.lock` resolved via
|
||||
/// [`manifest::paths::scope_lock_path`]. Tests should point this
|
||||
/// elsewhere by setting `INSOMNIA_HOME` or `INSOMNIA_RUNTIME_DIR` to a
|
||||
/// tempdir.
|
||||
pub fn default_lock_path() -> io::Result<PathBuf> {
|
||||
if let Ok(custom) = std::env::var("INSOMNIA_SCOPE_LOCK") {
|
||||
return Ok(PathBuf::from(custom));
|
||||
}
|
||||
let base = if let Ok(dir) = std::env::var("XDG_RUNTIME_DIR") {
|
||||
PathBuf::from(dir).join("insomnia")
|
||||
} else if let Ok(home) = std::env::var("HOME") {
|
||||
PathBuf::from(home).join(".insomnia").join("run")
|
||||
} else {
|
||||
return Err(io::Error::new(
|
||||
paths::scope_lock_path().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"neither XDG_RUNTIME_DIR nor HOME is set",
|
||||
));
|
||||
};
|
||||
Ok(base.join("scope.lock"))
|
||||
"could not resolve scope.lock path (no INSOMNIA_HOME / \
|
||||
INSOMNIA_RUNTIME_DIR / XDG_RUNTIME_DIR / HOME)",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// RAII guard over an exclusively-locked lock file.
|
||||
|
|
@ -555,15 +546,67 @@ pub enum ScopeLockError {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use manifest::Permission;
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
use std::sync::{LazyLock, Mutex, MutexGuard};
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Serialises tests that mutate `INSOMNIA_SCOPE_LOCK`. The test
|
||||
/// Serialises tests that mutate runtime-dir env vars. The test
|
||||
/// harness runs tests on multiple threads inside a single process,
|
||||
/// so env-var writes from one test would otherwise leak into a
|
||||
/// parallel test's `default_lock_path()` lookup.
|
||||
static ENV_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
|
||||
|
||||
/// Sandbox `INSOMNIA_RUNTIME_DIR` to a tempdir for the duration of
|
||||
/// a test; restore the previous value (and any `INSOMNIA_HOME` /
|
||||
/// `XDG_RUNTIME_DIR` that would otherwise outrank it) on drop.
|
||||
struct RuntimeDirSandbox {
|
||||
prev_runtime: Option<String>,
|
||||
prev_home: Option<String>,
|
||||
prev_xdg: Option<String>,
|
||||
_guard: MutexGuard<'static, ()>,
|
||||
}
|
||||
|
||||
impl RuntimeDirSandbox {
|
||||
fn new(dir: &Path) -> Self {
|
||||
let guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let prev_runtime = std::env::var("INSOMNIA_RUNTIME_DIR").ok();
|
||||
let prev_home = std::env::var("INSOMNIA_HOME").ok();
|
||||
let prev_xdg = std::env::var("XDG_RUNTIME_DIR").ok();
|
||||
// SAFETY: ENV_LOCK serialises env writes across this test
|
||||
// module; other modules that touch env vars rely on their
|
||||
// own lock or `serial_test`.
|
||||
unsafe {
|
||||
std::env::remove_var("INSOMNIA_HOME");
|
||||
std::env::remove_var("XDG_RUNTIME_DIR");
|
||||
std::env::set_var("INSOMNIA_RUNTIME_DIR", dir);
|
||||
}
|
||||
Self {
|
||||
prev_runtime,
|
||||
prev_home,
|
||||
prev_xdg,
|
||||
_guard: guard,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RuntimeDirSandbox {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
match &self.prev_runtime {
|
||||
Some(v) => std::env::set_var("INSOMNIA_RUNTIME_DIR", v),
|
||||
None => std::env::remove_var("INSOMNIA_RUNTIME_DIR"),
|
||||
}
|
||||
match &self.prev_home {
|
||||
Some(v) => std::env::set_var("INSOMNIA_HOME", v),
|
||||
None => std::env::remove_var("INSOMNIA_HOME"),
|
||||
}
|
||||
match &self.prev_xdg {
|
||||
Some(v) => std::env::set_var("XDG_RUNTIME_DIR", v),
|
||||
None => std::env::remove_var("XDG_RUNTIME_DIR"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_rule(path: &str, recursive: bool) -> ScopeRule {
|
||||
ScopeRule {
|
||||
target: PathBuf::from(path),
|
||||
|
|
@ -977,12 +1020,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn scope_allocation_guard_releases_on_drop() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
let dir = TempDir::new().unwrap();
|
||||
let _sandbox = RuntimeDirSandbox::new(dir.path());
|
||||
let lock_path = dir.path().join("scope.lock");
|
||||
unsafe {
|
||||
std::env::set_var("INSOMNIA_SCOPE_LOCK", &lock_path);
|
||||
}
|
||||
let guard = install_top_level(
|
||||
"a".into(),
|
||||
std::process::id(),
|
||||
|
|
@ -999,19 +1039,13 @@ mod tests {
|
|||
let g = LockFileGuard::open(&lock_path).unwrap();
|
||||
assert!(g.data().find("a").is_none());
|
||||
}
|
||||
unsafe {
|
||||
std::env::remove_var("INSOMNIA_SCOPE_LOCK");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adopt_allocation_rewrites_pid_and_releases_on_drop() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
let dir = TempDir::new().unwrap();
|
||||
let _sandbox = RuntimeDirSandbox::new(dir.path());
|
||||
let lock_path = dir.path().join("scope.lock");
|
||||
unsafe {
|
||||
std::env::set_var("INSOMNIA_SCOPE_LOCK", &lock_path);
|
||||
}
|
||||
// Pre-register an allocation under spawner's pid, as delegate_scope would.
|
||||
{
|
||||
let mut g = LockFileGuard::open(&lock_path).unwrap();
|
||||
|
|
@ -1029,24 +1063,14 @@ mod tests {
|
|||
let g = LockFileGuard::open(&lock_path).unwrap();
|
||||
assert!(g.data().find("child").is_none());
|
||||
}
|
||||
unsafe {
|
||||
std::env::remove_var("INSOMNIA_SCOPE_LOCK");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adopt_allocation_errors_on_unknown_pod() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
let dir = TempDir::new().unwrap();
|
||||
let lock_path = dir.path().join("scope.lock");
|
||||
unsafe {
|
||||
std::env::set_var("INSOMNIA_SCOPE_LOCK", &lock_path);
|
||||
}
|
||||
let _sandbox = RuntimeDirSandbox::new(dir.path());
|
||||
let err = adopt_allocation("ghost".into(), 42).unwrap_err();
|
||||
assert!(matches!(err, ScopeLockError::UnknownPod(ref n) if n == "ghost"));
|
||||
unsafe {
|
||||
std::env::remove_var("INSOMNIA_SCOPE_LOCK");
|
||||
}
|
||||
}
|
||||
|
||||
/// Mimic what the spawner does before the child comes up: push an
|
||||
|
|
|
|||
|
|
@ -28,17 +28,47 @@ use tokio::net::UnixListener;
|
|||
use tokio::task::JoinHandle;
|
||||
|
||||
/// Serialises env-mutating tests. The test harness runs tasks across
|
||||
/// threads, and `INSOMNIA_SCOPE_LOCK` is a process-wide resource.
|
||||
/// threads, and `INSOMNIA_RUNTIME_DIR` is a process-wide resource.
|
||||
static ENV_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
|
||||
|
||||
/// Take `ENV_LOCK` and clear any env vars that would outrank
|
||||
/// `INSOMNIA_RUNTIME_DIR` in `paths::runtime_dir` resolution; restore
|
||||
/// previous values on drop.
|
||||
struct EnvGuard {
|
||||
prev_home: Option<String>,
|
||||
prev_xdg: Option<String>,
|
||||
_lock: std::sync::MutexGuard<'static, ()>,
|
||||
}
|
||||
|
||||
impl EnvGuard {
|
||||
fn acquire() -> Self {
|
||||
let lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let prev_home = std::env::var("INSOMNIA_HOME").ok();
|
||||
let prev_xdg = std::env::var("XDG_RUNTIME_DIR").ok();
|
||||
unsafe {
|
||||
std::env::remove_var("INSOMNIA_HOME");
|
||||
std::env::remove_var("XDG_RUNTIME_DIR");
|
||||
}
|
||||
Self {
|
||||
_lock: ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()),
|
||||
prev_home,
|
||||
prev_xdg,
|
||||
_lock: lock,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvGuard {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
match &self.prev_home {
|
||||
Some(v) => std::env::set_var("INSOMNIA_HOME", v),
|
||||
None => std::env::remove_var("INSOMNIA_HOME"),
|
||||
}
|
||||
match &self.prev_xdg {
|
||||
Some(v) => std::env::set_var("XDG_RUNTIME_DIR", v),
|
||||
None => std::env::remove_var("XDG_RUNTIME_DIR"),
|
||||
}
|
||||
std::env::remove_var("INSOMNIA_RUNTIME_DIR");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -293,10 +323,10 @@ async fn read_pod_output_reports_stopped_on_dead_socket() {
|
|||
async fn stop_pod_sends_shutdown_and_releases_scope() {
|
||||
let _env = EnvGuard::acquire();
|
||||
let (tmp, registry, rd) = setup_registry().await;
|
||||
let lock_path = tmp.path().join("scope.lock");
|
||||
unsafe {
|
||||
std::env::set_var("INSOMNIA_SCOPE_LOCK", &lock_path);
|
||||
std::env::set_var("INSOMNIA_RUNTIME_DIR", tmp.path());
|
||||
}
|
||||
let lock_path = tmp.path().join("scope.lock");
|
||||
|
||||
// Seed scope.lock with a top-level `spawner` allocation plus a
|
||||
// delegated `child` allocation — mimics what SpawnPod would have
|
||||
|
|
@ -356,19 +386,14 @@ async fn stop_pod_sends_shutdown_and_releases_scope() {
|
|||
let contents = std::fs::read_to_string(&spawned).unwrap();
|
||||
let records: Vec<SpawnedPodRecord> = serde_json::from_str(&contents).unwrap();
|
||||
assert!(records.is_empty());
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("INSOMNIA_SCOPE_LOCK");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stop_pod_succeeds_even_when_child_unreachable() {
|
||||
let _env = EnvGuard::acquire();
|
||||
let (tmp, registry, _rd) = setup_registry().await;
|
||||
let lock_path = tmp.path().join("scope.lock");
|
||||
unsafe {
|
||||
std::env::set_var("INSOMNIA_SCOPE_LOCK", &lock_path);
|
||||
std::env::set_var("INSOMNIA_RUNTIME_DIR", tmp.path());
|
||||
}
|
||||
|
||||
// No live listener — socket never bound. Registered record points
|
||||
|
|
@ -384,10 +409,6 @@ async fn stop_pod_succeeds_even_when_child_unreachable() {
|
|||
|
||||
// Registry no longer knows about the child.
|
||||
assert!(registry.get("child").await.is_none());
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("INSOMNIA_SCOPE_LOCK");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -18,30 +18,61 @@ use protocol::{Method, Permission, PodEvent, ScopeRule};
|
|||
use tempfile::TempDir;
|
||||
use tokio::net::UnixListener;
|
||||
|
||||
/// Serialises tests that mutate `INSOMNIA_SCOPE_LOCK`.
|
||||
/// Serialises tests that mutate `INSOMNIA_RUNTIME_DIR`.
|
||||
static ENV_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
|
||||
|
||||
/// Take `ENV_LOCK` and clear any env vars that would outrank
|
||||
/// `INSOMNIA_RUNTIME_DIR`; restore previous values on drop.
|
||||
struct EnvGuard {
|
||||
prev_home: Option<String>,
|
||||
prev_xdg: Option<String>,
|
||||
_lock: std::sync::MutexGuard<'static, ()>,
|
||||
}
|
||||
|
||||
impl EnvGuard {
|
||||
fn acquire() -> Self {
|
||||
let lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let prev_home = std::env::var("INSOMNIA_HOME").ok();
|
||||
let prev_xdg = std::env::var("XDG_RUNTIME_DIR").ok();
|
||||
unsafe {
|
||||
std::env::remove_var("INSOMNIA_HOME");
|
||||
std::env::remove_var("XDG_RUNTIME_DIR");
|
||||
}
|
||||
Self {
|
||||
_lock: ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()),
|
||||
prev_home,
|
||||
prev_xdg,
|
||||
_lock: lock,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_scope_lock_path(path: &std::path::Path) {
|
||||
unsafe {
|
||||
std::env::set_var("INSOMNIA_SCOPE_LOCK", path);
|
||||
impl Drop for EnvGuard {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
match &self.prev_home {
|
||||
Some(v) => std::env::set_var("INSOMNIA_HOME", v),
|
||||
None => std::env::remove_var("INSOMNIA_HOME"),
|
||||
}
|
||||
match &self.prev_xdg {
|
||||
Some(v) => std::env::set_var("XDG_RUNTIME_DIR", v),
|
||||
None => std::env::remove_var("XDG_RUNTIME_DIR"),
|
||||
}
|
||||
std::env::remove_var("INSOMNIA_RUNTIME_DIR");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_scope_lock_path() {
|
||||
/// Point `INSOMNIA_RUNTIME_DIR` at `dir`. The scope-lock then lives at
|
||||
/// `<dir>/scope.lock` and Pod runtime sub-dirs at `<dir>/{pod_name}/`.
|
||||
fn set_runtime_dir(dir: &std::path::Path) {
|
||||
unsafe {
|
||||
std::env::remove_var("INSOMNIA_SCOPE_LOCK");
|
||||
std::env::set_var("INSOMNIA_RUNTIME_DIR", dir);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_runtime_dir() {
|
||||
unsafe {
|
||||
std::env::remove_var("INSOMNIA_RUNTIME_DIR");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,7 +164,7 @@ async fn fresh_registry(runtime_base: &std::path::Path, pod_name: &str) -> Arc<S
|
|||
async fn apply_shutdown_removes_from_registry_and_tolerates_missing() {
|
||||
let _env = EnvGuard::acquire();
|
||||
let scope_dir = TempDir::new().unwrap();
|
||||
set_scope_lock_path(&scope_dir.path().join("scope.lock"));
|
||||
set_runtime_dir(scope_dir.path());
|
||||
|
||||
let runtime_base = TempDir::new().unwrap();
|
||||
let registry = fresh_registry(runtime_base.path(), "parent").await;
|
||||
|
|
@ -161,14 +192,14 @@ async fn apply_shutdown_removes_from_registry_and_tolerates_missing() {
|
|||
apply_event_side_effects(&event, ®istry, "parent", &None).await;
|
||||
assert!(registry.get("child").await.is_none());
|
||||
|
||||
clear_scope_lock_path();
|
||||
clear_runtime_dir();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_scope_sub_delegated_adds_grandchild_then_duplicate_is_noop() {
|
||||
let _env = EnvGuard::acquire();
|
||||
let scope_dir = TempDir::new().unwrap();
|
||||
set_scope_lock_path(&scope_dir.path().join("scope.lock"));
|
||||
set_runtime_dir(scope_dir.path());
|
||||
|
||||
let runtime_base = TempDir::new().unwrap();
|
||||
let registry = fresh_registry(runtime_base.path(), "grandparent").await;
|
||||
|
|
@ -208,14 +239,14 @@ async fn apply_scope_sub_delegated_adds_grandchild_then_duplicate_is_noop() {
|
|||
let gc2 = registry.get("grandchild").await.unwrap();
|
||||
assert_eq!(gc2.socket_path, PathBuf::from("/tmp/grandchild.sock"));
|
||||
|
||||
clear_scope_lock_path();
|
||||
clear_runtime_dir();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_scope_sub_delegated_reemits_to_own_parent() {
|
||||
let _env = EnvGuard::acquire();
|
||||
let scope_dir = TempDir::new().unwrap();
|
||||
set_scope_lock_path(&scope_dir.path().join("scope.lock"));
|
||||
set_runtime_dir(scope_dir.path());
|
||||
|
||||
let runtime_base = TempDir::new().unwrap();
|
||||
let registry = fresh_registry(runtime_base.path(), "B").await;
|
||||
|
|
@ -268,14 +299,14 @@ async fn apply_scope_sub_delegated_reemits_to_own_parent() {
|
|||
other => panic!("expected re-emitted ScopeSubDelegated, got {other:?}"),
|
||||
}
|
||||
|
||||
clear_scope_lock_path();
|
||||
clear_runtime_dir();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_turn_ended_and_errored_are_system_noops() {
|
||||
let _env = EnvGuard::acquire();
|
||||
let scope_dir = TempDir::new().unwrap();
|
||||
set_scope_lock_path(&scope_dir.path().join("scope.lock"));
|
||||
set_runtime_dir(scope_dir.path());
|
||||
|
||||
let runtime_base = TempDir::new().unwrap();
|
||||
let registry = fresh_registry(runtime_base.path(), "parent").await;
|
||||
|
|
@ -312,7 +343,7 @@ async fn apply_turn_ended_and_errored_are_system_noops() {
|
|||
.await;
|
||||
|
||||
assert!(registry.get("child").await.is_some());
|
||||
clear_scope_lock_path();
|
||||
clear_runtime_dir();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -320,7 +351,7 @@ async fn shutdown_releases_scope_allocation_when_present() {
|
|||
let _env = EnvGuard::acquire();
|
||||
let scope_dir = TempDir::new().unwrap();
|
||||
let lock_path = scope_dir.path().join("scope.lock");
|
||||
set_scope_lock_path(&lock_path);
|
||||
set_runtime_dir(scope_dir.path());
|
||||
|
||||
// Install a top-level allocation for "kid" so ShutDown has
|
||||
// something to release.
|
||||
|
|
@ -362,5 +393,5 @@ async fn shutdown_releases_scope_allocation_when_present() {
|
|||
"ShutDown should have released the scope allocation"
|
||||
);
|
||||
|
||||
clear_scope_lock_path();
|
||||
clear_runtime_dir();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ use std::sync::Arc;
|
|||
use tempfile::TempDir;
|
||||
use tokio::net::UnixListener;
|
||||
|
||||
/// Serialises tests that mutate `INSOMNIA_SCOPE_LOCK` /
|
||||
/// Serialises tests that mutate `INSOMNIA_RUNTIME_DIR` /
|
||||
/// `INSOMNIA_POD_COMMAND` across the thread-pooled test harness.
|
||||
static ENV_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
|
||||
|
||||
|
|
@ -39,22 +39,26 @@ impl EnvGuard {
|
|||
}
|
||||
}
|
||||
|
||||
/// Set up a tempdir, point `INSOMNIA_SCOPE_LOCK` + runtime-dir base at
|
||||
/// it, and install a live top-level "spawner" allocation so the tool
|
||||
/// has something to delegate from. Returns the tempdir (keeps it alive
|
||||
/// for the test's lifetime), runtime base, spawner socket, and the
|
||||
/// spawner's runtime dir.
|
||||
/// Set up a tempdir, point `INSOMNIA_RUNTIME_DIR` at it (so
|
||||
/// `scope.lock` and per-Pod runtime subdirs both land in the
|
||||
/// sandbox), and install a live top-level "spawner" allocation so the
|
||||
/// tool has something to delegate from. Returns the tempdir (keeps it
|
||||
/// alive for the test's lifetime), runtime base, spawner socket, and
|
||||
/// the spawner's runtime dir.
|
||||
async fn setup_spawner(
|
||||
spawner_name: &str,
|
||||
allow_root: &Path,
|
||||
) -> (TempDir, PathBuf, PathBuf, Arc<RuntimeDir>) {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let lock_path = tmp.path().join("scope.lock");
|
||||
let runtime_base = tmp.path().to_path_buf();
|
||||
unsafe {
|
||||
std::env::set_var("INSOMNIA_SCOPE_LOCK", &lock_path);
|
||||
// Outranking env vars must be cleared so `paths::runtime_dir`
|
||||
// resolves to our sandbox instead of the developer's real one.
|
||||
std::env::remove_var("INSOMNIA_HOME");
|
||||
std::env::remove_var("XDG_RUNTIME_DIR");
|
||||
std::env::set_var("INSOMNIA_RUNTIME_DIR", &runtime_base);
|
||||
}
|
||||
|
||||
let runtime_base = tmp.path().join("runtime");
|
||||
let spawner_rd = RuntimeDir::create(&runtime_base, spawner_name)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -148,7 +152,7 @@ fn dummy_model() -> ModelManifest {
|
|||
|
||||
fn clear_env() {
|
||||
unsafe {
|
||||
std::env::remove_var("INSOMNIA_SCOPE_LOCK");
|
||||
std::env::remove_var("INSOMNIA_RUNTIME_DIR");
|
||||
std::env::remove_var("INSOMNIA_POD_COMMAND");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
//!
|
||||
//! - builtin プロバイダ: `resources/providers/builtin.toml`
|
||||
//! - builtin モデル: `resources/models/builtin.toml`
|
||||
//! - user override: `$XDG_CONFIG_HOME/insomnia/{providers,models}.toml`
|
||||
//! - user override: `<config_dir>/{providers,models}.toml`
|
||||
//!
|
||||
//! `<config_dir>` の解決は [`manifest::paths::config_dir`] を参照。
|
||||
//! どちらの override も「あれば builtin を置換、無ければ builtin」と
|
||||
//! いう一方向の差し替え(マージしない)。providers / models は独立に
|
||||
//! 読み、片方だけ user override も可。
|
||||
|
|
@ -150,13 +151,13 @@ fn auth_hint_to_ref(hint: &AuthHint) -> AuthRef {
|
|||
|
||||
/// builtin + user override を解決して provider カタログを返す。
|
||||
///
|
||||
/// user override (`$XDG_CONFIG_HOME/insomnia/providers.toml`) が
|
||||
/// 存在すれば builtin を置き換える。存在しなければ builtin のみ。
|
||||
/// user override が存在するが壊れている場合はエラーを返す(silent
|
||||
/// fallback はしない — ユーザーが書いた設定が silent に無視されて
|
||||
/// builtin に戻る挙動は気付きにくいため)。
|
||||
/// user override (`<config_dir>/providers.toml`) が存在すれば builtin
|
||||
/// を置き換える。存在しなければ builtin のみ。user override が存在
|
||||
/// するが壊れている場合はエラーを返す(silent fallback はしない —
|
||||
/// ユーザーが書いた設定が silent に無視されて builtin に戻る挙動は
|
||||
/// 気付きにくいため)。
|
||||
pub fn load_providers() -> Result<Vec<ProviderEntry>, CatalogError> {
|
||||
if let Some(path) = user_override_path("providers.toml")
|
||||
if let Some(path) = manifest::paths::user_catalog_override("providers.toml")
|
||||
&& path.is_file()
|
||||
{
|
||||
return load_providers_from(&path);
|
||||
|
|
@ -189,7 +190,7 @@ pub fn load_providers_from(path: &Path) -> Result<Vec<ProviderEntry>, CatalogErr
|
|||
|
||||
/// builtin + user override を解決してモデルカタログを返す。
|
||||
pub fn load_models() -> Result<Vec<ModelEntry>, CatalogError> {
|
||||
if let Some(path) = user_override_path("models.toml")
|
||||
if let Some(path) = manifest::paths::user_catalog_override("models.toml")
|
||||
&& path.is_file()
|
||||
{
|
||||
return load_models_from(&path);
|
||||
|
|
@ -324,25 +325,6 @@ pub fn resolve_with_catalogs(
|
|||
}
|
||||
}
|
||||
|
||||
fn user_override_path(file_name: &str) -> Option<PathBuf> {
|
||||
if let Ok(dir) = std::env::var("XDG_CONFIG_HOME")
|
||||
&& !dir.is_empty()
|
||||
{
|
||||
return Some(PathBuf::from(dir).join("insomnia").join(file_name));
|
||||
}
|
||||
if let Ok(home) = std::env::var("HOME")
|
||||
&& !home.is_empty()
|
||||
{
|
||||
return Some(
|
||||
PathBuf::from(home)
|
||||
.join(".config")
|
||||
.join("insomnia")
|
||||
.join(file_name),
|
||||
);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -538,14 +520,41 @@ auth_hint = { kind = "none" }
|
|||
assert!(matches!(err, CatalogError::Parse { .. }));
|
||||
}
|
||||
|
||||
/// `INSOMNIA_CONFIG_DIR` を tempdir に向けるテストガード。
|
||||
/// `paths::config_dir` は他の env (INSOMNIA_HOME / XDG_CONFIG_HOME)
|
||||
/// より高優先で `INSOMNIA_CONFIG_DIR` を尊重するため、これだけで
|
||||
/// 開発機の env 設定に左右されないテストになる。
|
||||
struct ConfigDirGuard {
|
||||
prev: Option<String>,
|
||||
}
|
||||
|
||||
impl ConfigDirGuard {
|
||||
fn new(path: &Path) -> Self {
|
||||
let prev = std::env::var("INSOMNIA_CONFIG_DIR").ok();
|
||||
// SAFETY: serial_test の `#[serial]` 属性で env を弄るテスト
|
||||
// 同士は直列化される。
|
||||
unsafe { std::env::set_var("INSOMNIA_CONFIG_DIR", path) };
|
||||
Self { prev }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ConfigDirGuard {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
match &self.prev {
|
||||
Some(v) => std::env::set_var("INSOMNIA_CONFIG_DIR", v),
|
||||
None => std::env::remove_var("INSOMNIA_CONFIG_DIR"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn load_prefers_user_override() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let insomnia_dir = dir.path().join("insomnia");
|
||||
std::fs::create_dir_all(&insomnia_dir).unwrap();
|
||||
std::fs::write(
|
||||
insomnia_dir.join("providers.toml"),
|
||||
dir.path().join("providers.toml"),
|
||||
r#"
|
||||
[[provider]]
|
||||
id = "only-one"
|
||||
|
|
@ -556,14 +565,8 @@ auth_hint = { kind = "none" }
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let prev_xdg = std::env::var("XDG_CONFIG_HOME").ok();
|
||||
unsafe { std::env::set_var("XDG_CONFIG_HOME", dir.path()) };
|
||||
let _g = ConfigDirGuard::new(dir.path());
|
||||
let entries = load_providers().unwrap();
|
||||
match prev_xdg {
|
||||
Some(v) => unsafe { std::env::set_var("XDG_CONFIG_HOME", v) },
|
||||
None => unsafe { std::env::remove_var("XDG_CONFIG_HOME") },
|
||||
}
|
||||
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].id, "only-one");
|
||||
}
|
||||
|
|
@ -573,13 +576,8 @@ auth_hint = { kind = "none" }
|
|||
fn load_falls_back_to_builtin_when_override_absent() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
// override ファイルは作らない
|
||||
let prev_xdg = std::env::var("XDG_CONFIG_HOME").ok();
|
||||
unsafe { std::env::set_var("XDG_CONFIG_HOME", dir.path()) };
|
||||
let _g = ConfigDirGuard::new(dir.path());
|
||||
let entries = load_providers().unwrap();
|
||||
match prev_xdg {
|
||||
Some(v) => unsafe { std::env::set_var("XDG_CONFIG_HOME", v) },
|
||||
None => unsafe { std::env::remove_var("XDG_CONFIG_HOME") },
|
||||
}
|
||||
assert_eq!(entries.len(), 4);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,23 +33,8 @@ fn resolve_socket(pod_name: &str, override_path: Option<PathBuf>) -> PathBuf {
|
|||
if let Some(p) = override_path {
|
||||
return p;
|
||||
}
|
||||
if let Ok(rd) = std::env::var("XDG_RUNTIME_DIR") {
|
||||
PathBuf::from(rd)
|
||||
.join("insomnia")
|
||||
.join(pod_name)
|
||||
.join("sock")
|
||||
} else if let Ok(home) = std::env::var("HOME") {
|
||||
PathBuf::from(home)
|
||||
.join(".insomnia")
|
||||
.join("run")
|
||||
.join(pod_name)
|
||||
.join("sock")
|
||||
} else {
|
||||
PathBuf::from("/tmp")
|
||||
.join("insomnia")
|
||||
.join(pod_name)
|
||||
.join("sock")
|
||||
}
|
||||
manifest::paths::pod_socket_path(pod_name)
|
||||
.unwrap_or_else(|| PathBuf::from("/tmp").join("insomnia").join(pod_name).join("sock"))
|
||||
}
|
||||
|
||||
enum Mode {
|
||||
|
|
|
|||
51
tickets/worker-generation-settings.md
Normal file
51
tickets/worker-generation-settings.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# LLM 生成設定の manifest 露出整理
|
||||
|
||||
## 背景
|
||||
|
||||
`llm-worker::RequestConfig` には LLM 生成に関する共通設定として `max_tokens` / `temperature` / `top_p` / `top_k` / `stop_sequences` / `reasoning` がある。一方、Pod manifest の `[worker]` まで露出しているのは現状 `max_tokens` と `temperature` のみで、`top_p` / `top_k` / `stop_sequences` は manifest から指定できない。
|
||||
|
||||
`reasoning` は `tickets/model-reasoning-control.md` で内部抽象と manifest 経路を整理するため、本チケットではそれ以外の既存 `RequestConfig` 項目を manifest から指定できるようにする。
|
||||
|
||||
## 要件
|
||||
|
||||
### `[worker]` への生成設定追加
|
||||
|
||||
Pod manifest の `[worker]` セクションで、既存 `RequestConfig` に存在する以下の項目を指定できるようにする。
|
||||
|
||||
```toml
|
||||
[worker]
|
||||
top_p = 0.9
|
||||
top_k = 40
|
||||
stop_sequences = ["\n\n", "</stop>"]
|
||||
```
|
||||
|
||||
- `top_p`: `Option<f32>` として扱い、指定時のみ request に渡す
|
||||
- `top_k`: `Option<u32>` として扱い、指定時のみ request に渡す
|
||||
- `stop_sequences`: `Vec<String>` として扱い、未指定時は空配列と同等にする
|
||||
|
||||
Provider / scheme によって効かない値がある場合でも、既存の scheme 投影に任せる。値の厳密な provider 別検証は本チケットでは行わない。
|
||||
|
||||
### manifest cascade / restore 経路
|
||||
|
||||
`WorkerManifestConfig` / `WorkerManifest` に上記フィールドを追加し、manifest cascade の merge 後に `pod::apply_worker_manifest()` から `RequestConfig` へ渡す。
|
||||
|
||||
`stop_sequences` は cascade 時に上位 manifest が指定した場合の意味を明確にする。初期方針は、`max_tokens` や `temperature` と同様に「上位指定があれば置き換え」とし、配列の追記マージは行わない。
|
||||
|
||||
### 既存挙動の保持
|
||||
|
||||
未指定時は現在と同じ request body になること。特に、既存 manifest で `top_p` / `top_k` / `stop_sequences` を省略している場合、wire request に新しい値が出ない。
|
||||
|
||||
## 範囲外
|
||||
|
||||
- `reasoning` / thinking 制御の manifest 露出。これは `tickets/model-reasoning-control.md` で扱う
|
||||
- `presence_penalty` / `frequency_penalty` / `seed` / `response_format` / `tool_choice` / `parallel_tool_calls` 等、まだ `RequestConfig` に存在しない新規生成パラメータの追加
|
||||
- provider ごとの値域検証や推奨値テーブル
|
||||
- UI での設定編集画面
|
||||
|
||||
## 完了条件
|
||||
|
||||
- `[worker].top_p` / `[worker].top_k` / `[worker].stop_sequences` を manifest TOML で指定できる
|
||||
- cascade merge 後の `WorkerManifest` がこれらの値を保持する
|
||||
- `pod::apply_worker_manifest()` が `RequestConfig.top_p` / `top_k` / `stop_sequences` へ値を渡す
|
||||
- 未指定時の既存挙動が変わらない
|
||||
- manifest parse / merge / apply のテストが追加または更新されている
|
||||
Loading…
Reference in New Issue
Block a user