diff --git a/Cargo.lock b/Cargo.lock index e25bc661..e3fdc316 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2127,10 +2127,10 @@ dependencies = [ "manifest", "memory", "minijinja", + "pod-registry", "protocol", "provider", "schemars", - "scope-lock", "serde", "serde_json", "session-store", @@ -2142,6 +2142,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "pod-registry" +version = "0.1.0" +dependencies = [ + "fs4", + "libc", + "manifest", + "serde", + "serde_json", + "session-store", + "tempfile", + "thiserror 2.0.18", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -2747,20 +2761,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "scope-lock" -version = "0.1.0" -dependencies = [ - "fs4", - "libc", - "manifest", - "serde", - "serde_json", - "session-store", - "tempfile", - "thiserror 2.0.18", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -3587,9 +3587,9 @@ version = "0.1.0" dependencies = [ "crossterm 0.28.1", "manifest", + "pod-registry", "protocol", "ratatui", - "scope-lock", "serde_json", "session-store", "tokio", diff --git a/Cargo.toml b/Cargo.toml index f871a5ec..1728dcf8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ "crates/pod", "crates/protocol", "crates/provider", - "crates/scope-lock", + "crates/pod-registry", "crates/tools", "crates/tui", "crates/memory", ] diff --git a/crates/manifest/src/paths.rs b/crates/manifest/src/paths.rs index 081ce3d5..96f9873f 100644 --- a/crates/manifest/src/paths.rs +++ b/crates/manifest/src/paths.rs @@ -6,7 +6,7 @@ //! `providers.toml`, `models.toml`, `prompts/`, `prompts.toml` 等 //! - **`data_dir`** — プログラムが書く永続データ。`sessions/` 等 //! - **`runtime_dir`** — 再起動で消えてよいランタイム状態。socket, -//! `scope.lock`, `pid` ファイル等 +//! `pods.json`, `pid` ファイル等 //! //! ## 解決順 (優先順位高 → 低) //! @@ -52,7 +52,7 @@ pub fn data_dir() -> Option { Some(env_path("HOME")?.join(".insomnia")) } -/// ランタイムディレクトリ。socket, `scope.lock`, Pod ごとの `pid` / +/// ランタイムディレクトリ。socket, `pods.json`, Pod ごとの `pid` / /// `status.json` 等が置かれる。再起動で消えて構わない。 pub fn runtime_dir() -> Option { if let Some(p) = env_path("INSOMNIA_RUNTIME_DIR") { @@ -95,9 +95,9 @@ pub fn sessions_dir() -> Option { Some(data_dir()?.join("sessions")) } -/// `/scope.lock` — machine-wide scope allocation registry。 -pub fn scope_lock_path() -> Option { - Some(runtime_dir()?.join("scope.lock")) +/// `/pods.json` — machine-wide Pod allocation registry。 +pub fn pod_registry_path() -> Option { + Some(runtime_dir()?.join("pods.json")) } /// `//` — Pod ごとのランタイムディレクトリ。 @@ -302,8 +302,8 @@ mod tests { ); assert_eq!(sessions_dir().unwrap(), PathBuf::from("/sand/sessions")); assert_eq!( - scope_lock_path().unwrap(), - PathBuf::from("/sand/run/scope.lock") + pod_registry_path().unwrap(), + PathBuf::from("/sand/run/pods.json") ); assert_eq!( pod_runtime_dir("foo").unwrap(), diff --git a/crates/manifest/src/scope.rs b/crates/manifest/src/scope.rs index 0532dc87..2d7b3a8f 100644 --- a/crates/manifest/src/scope.rs +++ b/crates/manifest/src/scope.rs @@ -142,7 +142,7 @@ impl Scope { /// Allow rules with their targets resolved to absolute paths. /// - /// Used by the scope-lock registry, where every Pod's allocation + /// Used by the pod-registry, where every Pod's allocation /// must be expressed in absolute terms so prefix comparisons are /// meaningful across processes. pub fn allow_rules(&self) -> Vec { diff --git a/crates/scope-lock/Cargo.toml b/crates/pod-registry/Cargo.toml similarity index 94% rename from crates/scope-lock/Cargo.toml rename to crates/pod-registry/Cargo.toml index 3c88c03b..5cbcc9d1 100644 --- a/crates/scope-lock/Cargo.toml +++ b/crates/pod-registry/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "scope-lock" +name = "pod-registry" version = "0.1.0" edition.workspace = true license.workspace = true diff --git a/crates/scope-lock/src/lib.rs b/crates/pod-registry/src/lib.rs similarity index 96% rename from crates/scope-lock/src/lib.rs rename to crates/pod-registry/src/lib.rs index 5e7a5187..690cbdfa 100644 --- a/crates/scope-lock/src/lib.rs +++ b/crates/pod-registry/src/lib.rs @@ -1,7 +1,7 @@ -//! Machine-wide scope allocation registry. +//! Machine-wide Pod allocation registry. //! -//! A single JSON file at `/scope.lock` records every live -//! Pod's scope allocation (see [`manifest::paths::scope_lock_path`] for +//! A single JSON file at `/pods.json` records every live +//! Pod's allocation (see [`manifest::paths::pod_registry_path`] for //! how the path is resolved). File-level `flock(2)` serialises access //! across processes so spawn sequences from unrelated Pods can't race. //! @@ -77,15 +77,15 @@ impl LockFile { } } -/// Default on-disk path: `/scope.lock` resolved via -/// [`manifest::paths::scope_lock_path`]. Tests should point this +/// Default on-disk path: `/pods.json` resolved via +/// [`manifest::paths::pod_registry_path`]. Tests should point this /// elsewhere by setting `INSOMNIA_HOME` or `INSOMNIA_RUNTIME_DIR` to a /// tempdir. -pub fn default_lock_path() -> io::Result { - paths::scope_lock_path().ok_or_else(|| { +pub fn default_registry_path() -> io::Result { + paths::pod_registry_path().ok_or_else(|| { io::Error::new( io::ErrorKind::NotFound, - "could not resolve scope.lock path (no INSOMNIA_HOME / \ + "could not resolve pods.json path (no INSOMNIA_HOME / \ INSOMNIA_RUNTIME_DIR / XDG_RUNTIME_DIR / HOME)", ) }) @@ -146,7 +146,7 @@ impl LockFileGuard { serde_json::from_str(&buf).map_err(|e| { io::Error::new( io::ErrorKind::InvalidData, - format!("scope.lock parse error: {e}"), + format!("pods.json parse error: {e}"), ) })? }; @@ -516,7 +516,7 @@ pub fn install_top_level( scope_allow: Vec, session_id: SessionId, ) -> Result { - let lock_path = default_lock_path()?; + let lock_path = default_registry_path()?; let mut guard = LockFileGuard::open(&lock_path)?; register_pod( &mut guard, @@ -546,7 +546,7 @@ pub fn adopt_allocation( new_pid: u32, session_id: SessionId, ) -> Result { - let lock_path = default_lock_path()?; + let lock_path = default_registry_path()?; let mut guard = LockFileGuard::open(&lock_path)?; let alloc = guard .data_mut() @@ -581,7 +581,7 @@ pub fn adopt_allocation( /// guard, so the session_id collision check is atomic with the /// rewrite. pub fn update_session(pod_name: &str, new_session_id: SessionId) -> Result<(), ScopeLockError> { - let lock_path = default_lock_path()?; + let lock_path = default_registry_path()?; let mut guard = LockFileGuard::open(&lock_path)?; if let Some(other) = guard.data().find_by_session(new_session_id) { if other.pod_name != pod_name { @@ -616,7 +616,7 @@ pub struct SessionLockInfo { /// Used by `Pod::restore_from_manifest` to refuse a resume that would /// race a live writer on the same source session. pub fn lookup_session(session_id: SessionId) -> Result, ScopeLockError> { - let lock_path = default_lock_path()?; + let lock_path = default_registry_path()?; let mut guard = LockFileGuard::open(&lock_path)?; reclaim_stale(&mut guard); Ok(guard.data().find_by_session(session_id).map(|a| { @@ -628,10 +628,10 @@ pub fn lookup_session(session_id: SessionId) -> Result, })) } -/// Errors raised by the mutating scope-lock operations. +/// Errors raised by the mutating pod-registry operations. #[derive(Debug, thiserror::Error)] pub enum ScopeLockError { - #[error("I/O error on scope.lock: {0}")] + #[error("I/O error on pods.json: {0}")] Io(#[from] io::Error), #[error("pod name `{0}` is already registered")] DuplicatePodName(String), @@ -743,7 +743,7 @@ mod tests { #[test] fn open_creates_empty_lock_file() { let dir = TempDir::new().unwrap(); - let path = dir.path().join("scope.lock"); + let path = dir.path().join("pods.json"); let guard = LockFileGuard::open(&path).unwrap(); assert!(guard.data().allocations.is_empty()); assert!(path.exists()); @@ -754,7 +754,7 @@ mod tests { use std::os::unix::fs::PermissionsExt; let dir = TempDir::new().unwrap(); let parent = dir.path().join("insomnia"); - let path = parent.join("scope.lock"); + let path = parent.join("pods.json"); let _guard = LockFileGuard::open(&path).unwrap(); let file_mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777; assert_eq!(file_mode, 0o600, "file mode = {file_mode:o}"); @@ -765,7 +765,7 @@ mod tests { #[test] fn save_and_reopen_roundtrip() { let dir = TempDir::new().unwrap(); - let path = dir.path().join("scope.lock"); + let path = dir.path().join("pods.json"); { let mut g = open_empty(&path); register_pod( @@ -814,7 +814,7 @@ mod tests { #[test] fn register_detects_write_conflict() { let dir = TempDir::new().unwrap(); - let path = dir.path().join("scope.lock"); + let path = dir.path().join("pods.json"); let mut g = open_empty(&path); register_pod( &mut g, @@ -843,7 +843,7 @@ mod tests { #[test] fn duplicate_pod_name_rejected() { let dir = TempDir::new().unwrap(); - let path = dir.path().join("scope.lock"); + let path = dir.path().join("pods.json"); let mut g = open_empty(&path); register_pod( &mut g, @@ -869,7 +869,7 @@ mod tests { #[test] fn delegate_must_be_subset() { let dir = TempDir::new().unwrap(); - let path = dir.path().join("scope.lock"); + let path = dir.path().join("pods.json"); let mut g = open_empty(&path); register_pod( &mut g, @@ -895,7 +895,7 @@ mod tests { #[test] fn delegate_succeeds_within_parent_scope() { let dir = TempDir::new().unwrap(); - let path = dir.path().join("scope.lock"); + let path = dir.path().join("pods.json"); let mut g = open_empty(&path); register_pod( &mut g, @@ -933,7 +933,7 @@ mod tests { #[test] fn delegate_rejects_sibling_overlap() { let dir = TempDir::new().unwrap(); - let path = dir.path().join("scope.lock"); + let path = dir.path().join("pods.json"); let mut g = open_empty(&path); register_pod( &mut g, @@ -970,7 +970,7 @@ mod tests { #[test] fn release_reparents_children() { let dir = TempDir::new().unwrap(); - let path = dir.path().join("scope.lock"); + let path = dir.path().join("pods.json"); let mut g = open_empty(&path); register_pod( &mut g, @@ -1009,7 +1009,7 @@ mod tests { #[test] fn reclaim_stale_reparents_and_removes_dead_entries() { let dir = TempDir::new().unwrap(); - let path = dir.path().join("scope.lock"); + let path = dir.path().join("pods.json"); let mut g = open_empty(&path); register_pod( &mut g, @@ -1063,7 +1063,7 @@ mod tests { #[test] fn read_rules_do_not_conflict_with_write() { let dir = TempDir::new().unwrap(); - let path = dir.path().join("scope.lock"); + let path = dir.path().join("pods.json"); let mut g = open_empty(&path); register_pod( &mut g, @@ -1090,7 +1090,7 @@ mod tests { #[test] fn releasing_pod_reopens_scope_for_fresh_registration() { let dir = TempDir::new().unwrap(); - let path = dir.path().join("scope.lock"); + let path = dir.path().join("pods.json"); let mut g = open_empty(&path); register_pod( &mut g, @@ -1116,7 +1116,7 @@ mod tests { #[test] fn delegated_scope_returns_to_parent_on_release() { let dir = TempDir::new().unwrap(); - let path = dir.path().join("scope.lock"); + let path = dir.path().join("pods.json"); let mut g = open_empty(&path); register_pod( &mut g, @@ -1154,7 +1154,7 @@ mod tests { fn scope_allocation_guard_releases_on_drop() { let dir = TempDir::new().unwrap(); let _sandbox = RuntimeDirSandbox::new(dir.path()); - let lock_path = dir.path().join("scope.lock"); + let lock_path = dir.path().join("pods.json"); let guard = install_top_level( "a".into(), std::process::id(), @@ -1178,7 +1178,7 @@ mod tests { fn adopt_allocation_rewrites_pid_and_releases_on_drop() { let dir = TempDir::new().unwrap(); let _sandbox = RuntimeDirSandbox::new(dir.path()); - let lock_path = dir.path().join("scope.lock"); + let lock_path = dir.path().join("pods.json"); // Pre-register an allocation under spawner's pid, as delegate_scope would. { let mut g = LockFileGuard::open(&lock_path).unwrap(); @@ -1224,7 +1224,7 @@ mod tests { #[test] fn conflict_detection_descends_to_real_owner() { let dir = TempDir::new().unwrap(); - let path = dir.path().join("scope.lock"); + let path = dir.path().join("pods.json"); let mut g = open_empty(&path); register_pod( &mut g, @@ -1264,7 +1264,7 @@ mod tests { #[test] fn find_by_session_skips_none_placeholders() { let dir = TempDir::new().unwrap(); - let path = dir.path().join("scope.lock"); + let path = dir.path().join("pods.json"); let mut g = open_empty(&path); // Pre-reservation: delegate_scope leaves session_id = None // until adopt_allocation rewrites it. find_by_session must not @@ -1306,7 +1306,7 @@ mod tests { #[test] fn register_pod_rejects_session_id_collision() { let dir = TempDir::new().unwrap(); - let path = dir.path().join("scope.lock"); + let path = dir.path().join("pods.json"); let mut g = open_empty(&path); let shared_session = sid(); register_pod( diff --git a/crates/pod/Cargo.toml b/crates/pod/Cargo.toml index ff4e2783..16817d45 100644 --- a/crates/pod/Cargo.toml +++ b/crates/pod/Cargo.toml @@ -12,7 +12,7 @@ session-store = { version = "0.1.0", path = "../session-store" } manifest = { version = "0.1.0", path = "../manifest" } protocol = { version = "0.1.0", path = "../protocol" } provider = { version = "0.1.0", path = "../provider" } -scope-lock = { version = "0.1.0", path = "../scope-lock" } +pod-registry = { version = "0.1.0", path = "../pod-registry" } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" thiserror = "2.0" diff --git a/crates/pod/src/ipc/event.rs b/crates/pod/src/ipc/event.rs index c08cbb24..9957d2ac 100644 --- a/crates/pod/src/ipc/event.rs +++ b/crates/pod/src/ipc/event.rs @@ -8,7 +8,7 @@ //! logging failures without blocking the child. //! - **Render** a variant into a human-readable string that the parent's //! LLM sees via the notification buffer. -//! - **Apply side effects** on the parent (registry / scope-lock +//! - **Apply side effects** on the parent (registry / pod-registry //! updates) so that the receive path is idempotent and tolerant of //! out-of-order delivery. //! @@ -27,7 +27,7 @@ use std::sync::Arc; use protocol::{Method, PodEvent, ScopeRule}; use crate::runtime::dir::SpawnedPodRecord; -use crate::runtime::scope_lock::{self, ScopeLockError}; +use crate::runtime::pod_registry::{self, ScopeLockError}; use crate::spawn::comm_tools::connect_and_send; use crate::spawn::registry::SpawnedPodRegistry; @@ -146,21 +146,21 @@ pub async fn apply_event_side_effects( } fn release_scope_silently(pod_name: &str) { - let lock_path = match scope_lock::default_lock_path() { + let lock_path = match pod_registry::default_registry_path() { Ok(p) => p, Err(e) => { - tracing::warn!(error = %e, "default_lock_path failed"); + tracing::warn!(error = %e, "default_registry_path failed"); return; } }; - let mut guard = match scope_lock::LockFileGuard::open(&lock_path) { + let mut guard = match pod_registry::LockFileGuard::open(&lock_path) { Ok(g) => g, Err(e) => { tracing::warn!(error = %e, "LockFileGuard open failed"); return; } }; - match scope_lock::release_pod(&mut guard, pod_name) { + match pod_registry::release_pod(&mut guard, pod_name) { Ok(()) => {} Err(ScopeLockError::UnknownPod(_)) => {} Err(e) => tracing::warn!(error = ?e, pod = %pod_name, "release_pod failed"), diff --git a/crates/pod/src/main.rs b/crates/pod/src/main.rs index 0aa85d5e..3acb247d 100644 --- a/crates/pod/src/main.rs +++ b/crates/pod/src/main.rs @@ -46,7 +46,7 @@ struct Cli { /// Restore a Pod from an existing session. The Pod re-uses the /// given session id and appends new turns to the same jsonl; - /// concurrent writers are prevented by the `scope.lock` registry. + /// concurrent writers are prevented by the pod-registry. /// Mutually exclusive with `--adopt` (spawned children always start /// fresh). #[arg(long, value_name = "UUID", conflicts_with = "adopt")] diff --git a/crates/pod/src/pod.rs b/crates/pod/src/pod.rs index 9de25981..7b1bc4b0 100644 --- a/crates/pod/src/pod.rs +++ b/crates/pod/src/pod.rs @@ -26,7 +26,7 @@ use crate::prompt::catalog::{CatalogError, PromptCatalog}; use crate::prompt::loader::PromptLoader; use crate::prompt::system::{SystemPromptContext, SystemPromptError, SystemPromptTemplate}; use crate::runtime::dir; -use crate::runtime::scope_lock::{self, ScopeAllocationGuard, ScopeLockError}; +use crate::runtime::pod_registry::{self, ScopeAllocationGuard, ScopeLockError}; use async_trait::async_trait; use llm_worker::interceptor::PreRequestAction; use protocol::{AlertLevel, AlertSource, Event, Segment}; @@ -727,11 +727,11 @@ impl Pod { ) .await?; // ensure_head_or_fork mints a fresh session_id when it auto- - // forks. Sync that to scope.lock so a concurrent + // forks. Sync that to pods.json so a concurrent // restore_from_manifest can't see "no live writer" for the new // session and grab it. if self.session_id != prev_session_id && self.scope_allocation.is_some() { - scope_lock::update_session(&self.manifest.pod.name, self.session_id)?; + pod_registry::update_session(&self.manifest.pod.name, self.session_id)?; } Ok(()) } @@ -1164,14 +1164,14 @@ impl Pod { // until its first LLM call. self.session_id = new_session_id; self.head_hash = Some(new_head_hash); - // Keep scope.lock pointing at the live session_id. Without this + // Keep pods.json pointing at the live session_id. Without this // a concurrent `restore_from_manifest(new_session_id)` would // see no live writer and grab the session this Pod just moved // into, causing two writers to race on the same jsonl. Skipped // when no allocation is installed (e.g. compact under // `Pod::new` in tests). if self.scope_allocation.is_some() { - scope_lock::update_session(&self.manifest.pod.name, new_session_id)?; + pod_registry::update_session(&self.manifest.pod.name, new_session_id)?; } let worker = self.worker.as_mut().unwrap(); worker.set_history(new_history); @@ -1493,18 +1493,18 @@ impl Pod, St> { // Session creation is deferred to the first run (see // `ensure_session_head`) so the SessionStart entry can capture // the rendered system prompt, not the raw template source. The - // session_id is allocated here so the scope-lock registration + // session_id is allocated here so the pod-registry registration // can record it from the start. let session_id = session_store::new_session_id(); - // Register this Pod in the machine-wide scope-lock registry + // Register this Pod in the machine-wide pod-registry // before building anything else, so a spawn that conflicts on // scope fails fast. let socket_path = dir::default_base() .map_err(ScopeLockError::from)? .join(&manifest.pod.name) .join("sock"); - let scope_allocation = scope_lock::install_top_level( + let scope_allocation = pod_registry::install_top_level( manifest.pod.name.clone(), std::process::id(), socket_path, @@ -1548,7 +1548,7 @@ impl Pod, St> { /// /// Behaves like [`Pod::from_manifest`] but claims the scope /// allocation that the spawner pre-registered via - /// [`scope_lock::delegate_scope`], rather than installing a new + /// [`pod_registry::delegate_scope`], rather than installing a new /// top-level entry. `callback_socket` carries the spawner's /// Unix-socket path so the spawned Pod can send `Method::Notify` /// back to the spawner. @@ -1562,7 +1562,7 @@ impl Pod, St> { let session_id = session_store::new_session_id(); let scope_allocation = - scope_lock::adopt_allocation(manifest.pod.name.clone(), std::process::id(), session_id)?; + pod_registry::adopt_allocation(manifest.pod.name.clone(), std::process::id(), session_id)?; let mut worker = Worker::new(common.client); apply_worker_manifest(&mut worker, &manifest.worker); @@ -1599,14 +1599,14 @@ impl Pod, St> { /// Restore a Pod from an existing session log. /// /// Resolves the manifest cascade exactly like [`Self::from_manifest`] - /// (pwd / scope / scope-lock / client / prompt catalog), seeds a + /// (pwd / scope / pod-registry / client / prompt catalog), seeds a /// fresh Worker from the source session's `RestoredState`, and /// reuses the same `session_id` so subsequent turns append to the /// source jsonl as a continuation of the same conversation. /// - /// Concurrent writers are prevented by the `scope.lock` registry: + /// Concurrent writers are prevented by the pod-registry: /// the registration carries `session_id`, and this constructor - /// refuses to start when `scope_lock::lookup_session` already finds + /// refuses to start when `pod_registry::lookup_session` already finds /// a live Pod writing to `session_id`. So there is no need to fork — /// resume is "the same session, a different process owning it". /// @@ -1636,7 +1636,7 @@ impl Pod, St> { .map_err(ScopeLockError::from)? .join(&manifest.pod.name) .join("sock"); - let scope_allocation = scope_lock::install_top_level( + let scope_allocation = pod_registry::install_top_level( manifest.pod.name.clone(), std::process::id(), socket_path, diff --git a/crates/pod/src/runtime/dir.rs b/crates/pod/src/runtime/dir.rs index f8276895..8ecbd9f1 100644 --- a/crates/pod/src/runtime/dir.rs +++ b/crates/pod/src/runtime/dir.rs @@ -11,7 +11,7 @@ use crate::shared_state::PodSharedState; /// /// Written by the spawner after a successful `SpawnPod` tool call so /// `ListPods` (future ticket) and a restored spawner can enumerate -/// their live children without re-querying `scope.lock`. +/// their live children without re-querying `pods.json`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SpawnedPodRecord { /// Spawned Pod's identity. diff --git a/crates/pod/src/runtime/mod.rs b/crates/pod/src/runtime/mod.rs index 6d331d14..e5cf84a8 100644 --- a/crates/pod/src/runtime/mod.rs +++ b/crates/pod/src/runtime/mod.rs @@ -1,2 +1,2 @@ pub mod dir; -pub use ::scope_lock; +pub use ::pod_registry; diff --git a/crates/pod/src/spawn/comm_tools.rs b/crates/pod/src/spawn/comm_tools.rs index b6d90943..33d16d70 100644 --- a/crates/pod/src/spawn/comm_tools.rs +++ b/crates/pod/src/spawn/comm_tools.rs @@ -22,7 +22,7 @@ use serde::Deserialize; use tokio::net::UnixStream; use crate::runtime::dir::SpawnedPodRecord; -use crate::runtime::scope_lock::{self, LockFileGuard}; +use crate::runtime::pod_registry::{self, LockFileGuard}; use crate::spawn::registry::SpawnedPodRegistry; /// Timeout applied to each socket-level operation — connect, write, @@ -283,10 +283,10 @@ impl Tool for ListPodsTool { // allocation table doesn't keep growing indefinitely when // children crash without a clean exit path. if !stale_names.is_empty() { - if let Ok(lock_path) = scope_lock::default_lock_path() + if let Ok(lock_path) = pod_registry::default_registry_path() && let Ok(mut guard) = LockFileGuard::open(&lock_path) { - scope_lock::reclaim_stale(&mut guard); + pod_registry::reclaim_stale(&mut guard); } } @@ -475,11 +475,11 @@ fn summarize_scope(record: &SpawnedPodRecord) -> String { /// effects (Method::Shutdown was sent), and stale-reclaim will clean /// up whatever we couldn't. fn release_scope(pod_name: &str) { - let Ok(lock_path) = scope_lock::default_lock_path() else { + let Ok(lock_path) = pod_registry::default_registry_path() else { return; }; let Ok(mut guard) = LockFileGuard::open(&lock_path) else { return; }; - let _ = scope_lock::release_pod(&mut guard, pod_name); + let _ = pod_registry::release_pod(&mut guard, pod_name); } diff --git a/crates/pod/src/spawn/tool.rs b/crates/pod/src/spawn/tool.rs index 530ba48e..b33c3340 100644 --- a/crates/pod/src/spawn/tool.rs +++ b/crates/pod/src/spawn/tool.rs @@ -1,9 +1,9 @@ //! `SpawnPod` tool — launch a new Pod process as a child of this one. //! -//! Wires scope-lock delegation, overlay-TOML construction, subprocess +//! Wires pod-registry delegation, overlay-TOML construction, subprocess //! launch, and socket handoff into a single `Tool` implementation. When //! the LLM calls `SpawnPod`, a fresh `pod` binary is exec'd in its own -//! process group, the scope lock is updated atomically, and the child's +//! process group, the pod-registry is updated atomically, and the child's //! first turn is kicked off by handing its socket a `Method::Run`. use std::path::{Path, PathBuf}; @@ -26,7 +26,7 @@ use tokio::time::sleep; use crate::ipc::event; use crate::runtime::dir::SpawnedPodRecord; -use crate::runtime::scope_lock::{self, LockFileGuard, ScopeLockError}; +use crate::runtime::pod_registry::{self, LockFileGuard, ScopeLockError}; use crate::spawn::registry::SpawnedPodRegistry; use protocol::PodEvent; @@ -93,7 +93,7 @@ impl From for Permission { /// controller once per Pod lifetime. pub struct SpawnPodTool { /// Spawner's own pod name — becomes the spawned Pod's - /// `delegated_from` in the scope-lock registry. + /// `delegated_from` in the pod-registry. spawner_name: String, /// Path to the spawner's Unix socket. Handed to the child via /// `--callback` so its `PodEvent` callbacks have somewhere to land. @@ -167,7 +167,7 @@ impl Tool for SpawnPodTool { .unwrap_or_else(|| DEFAULT_INSTRUCTION.to_string()); let predicted_socket = self.runtime_base.join(&input.name).join("sock"); - let lock_path = scope_lock::default_lock_path() + let lock_path = pod_registry::default_registry_path() .map_err(|e| ToolError::ExecutionFailed(format!("scope lock path: {e}")))?; // Reserve the allocation up front. Spawner's pid is a live @@ -175,7 +175,7 @@ impl Tool for SpawnPodTool { { let mut guard = LockFileGuard::open(&lock_path) .map_err(|e| ToolError::ExecutionFailed(format!("scope lock open: {e}")))?; - scope_lock::delegate_scope( + pod_registry::delegate_scope( &mut guard, &self.spawner_name, input.name.clone(), @@ -183,7 +183,7 @@ impl Tool for SpawnPodTool { predicted_socket.clone(), scope_allow.clone(), ) - .map_err(scope_lock_err_to_tool)?; + .map_err(pod_registry_err_to_tool)?; } // `start_outcome` covers steps that happen before the child is @@ -312,7 +312,7 @@ impl SpawnPodTool { fn release_reservation(&self, lock_path: &Path, pod_name: &str) { if let Ok(mut g) = LockFileGuard::open(lock_path) { - let _ = scope_lock::release_pod(&mut g, pod_name); + let _ = pod_registry::release_pod(&mut g, pod_name); } } } @@ -436,7 +436,7 @@ async fn send_run(socket: &Path, task: &str) -> Result<(), ToolError> { Ok(()) } -fn scope_lock_err_to_tool(e: ScopeLockError) -> ToolError { +fn pod_registry_err_to_tool(e: ScopeLockError) -> ToolError { match e { ScopeLockError::NotSubset { .. } | ScopeLockError::WriteConflict { .. } diff --git a/crates/pod/tests/pod_comm_tools_test.rs b/crates/pod/tests/pod_comm_tools_test.rs index dd194f43..7453dc5d 100644 --- a/crates/pod/tests/pod_comm_tools_test.rs +++ b/crates/pod/tests/pod_comm_tools_test.rs @@ -15,7 +15,7 @@ use llm_worker::llm_client::types::{ContentPart, Item, Role}; use llm_worker::tool::ToolOutput; use manifest::{Permission, ScopeRule}; use pod::runtime::dir::{RuntimeDir, SpawnedPodRecord}; -use pod::runtime::scope_lock::{self, LockFileGuard}; +use pod::runtime::pod_registry::{self, LockFileGuard}; use pod::spawn::comm_tools::{ list_pods_tool, read_pod_output_tool, send_to_pod_tool, stop_pod_tool, }; @@ -86,7 +86,7 @@ async fn setup_registry() -> (TempDir, Arc, Arc) /// Register a fake spawned-child record pointing at a given socket /// path, with a trivial write-scope for `scope_path`. Does not touch -/// scope.lock. +/// pods.json. async fn register_child( registry: &SpawnedPodRegistry, name: &str, @@ -334,14 +334,14 @@ async fn stop_pod_sends_shutdown_and_releases_scope() { unsafe { std::env::set_var("INSOMNIA_RUNTIME_DIR", tmp.path()); } - let lock_path = tmp.path().join("scope.lock"); + let lock_path = tmp.path().join("pods.json"); - // Seed scope.lock with a top-level `spawner` allocation plus a + // Seed pods.json with a top-level `spawner` allocation plus a // delegated `child` allocation — mimics what SpawnPod would have // done so StopPod has something to release. { let mut g = LockFileGuard::open(&lock_path).unwrap(); - scope_lock::register_pod( + pod_registry::register_pod( &mut g, "spawner".into(), std::process::id(), @@ -354,7 +354,7 @@ async fn stop_pod_sends_shutdown_and_releases_scope() { session_store::new_session_id(), ) .unwrap(); - scope_lock::delegate_scope( + pod_registry::delegate_scope( &mut g, "spawner", "child".into(), diff --git a/crates/pod/tests/pod_events_test.rs b/crates/pod/tests/pod_events_test.rs index 04abfb5c..bd3bccd1 100644 --- a/crates/pod/tests/pod_events_test.rs +++ b/crates/pod/tests/pod_events_test.rs @@ -11,7 +11,7 @@ use std::time::Duration; use pod::ipc::event::{apply_event_side_effects, fire_and_forget, render_event}; use pod::runtime::dir::{RuntimeDir, SpawnedPodRecord}; -use pod::runtime::scope_lock::{self, LockFileGuard}; +use pod::runtime::pod_registry::{self, LockFileGuard}; use pod::spawn::registry::SpawnedPodRegistry; use protocol::stream::JsonLineReader; use protocol::{Method, Permission, PodEvent, ScopeRule}; @@ -62,8 +62,8 @@ impl Drop for EnvGuard { } } -/// Point `INSOMNIA_RUNTIME_DIR` at `dir`. The scope-lock then lives at -/// `/scope.lock` and Pod runtime sub-dirs at `/{pod_name}/`. +/// Point `INSOMNIA_RUNTIME_DIR` at `dir`. The pod-registry then lives at +/// `/pods.json` and Pod runtime sub-dirs at `/{pod_name}/`. fn set_runtime_dir(dir: &std::path::Path) { unsafe { std::env::set_var("INSOMNIA_RUNTIME_DIR", dir); @@ -348,12 +348,12 @@ async fn apply_turn_ended_and_errored_are_system_noops() { 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"); + let lock_path = scope_dir.path().join("pods.json"); set_runtime_dir(scope_dir.path()); // Install a top-level allocation for "kid" so ShutDown has // something to release. - let guard = scope_lock::install_top_level( + let guard = pod_registry::install_top_level( "kid".into(), std::process::id(), "/tmp/kid.sock".into(), diff --git a/crates/pod/tests/restore_test.rs b/crates/pod/tests/restore_test.rs index e82ffd65..50cb51ba 100644 --- a/crates/pod/tests/restore_test.rs +++ b/crates/pod/tests/restore_test.rs @@ -2,7 +2,7 @@ //! validation paths. //! //! These cases all return before `prepare_pod_common` runs, so they -//! do not need a real LLM client or scope-lock environment — only the +//! do not need a real LLM client or pod-registry environment — only the //! session store needs to be present. use std::sync::{LazyLock, Mutex}; diff --git a/crates/pod/tests/spawn_pod_test.rs b/crates/pod/tests/spawn_pod_test.rs index 94929877..57141f9d 100644 --- a/crates/pod/tests/spawn_pod_test.rs +++ b/crates/pod/tests/spawn_pod_test.rs @@ -1,6 +1,6 @@ //! Integration tests for the `SpawnPod` tool. //! -//! These tests exercise the tool's scope-lock delegation, subprocess +//! These tests exercise the tool's pod-registry delegation, subprocess //! launch, socket handoff, and `spawned_pods.json` write without relying //! on the real `pod` binary. `INSOMNIA_POD_COMMAND` is pointed at //! `/bin/true` (which exits immediately) while a test-owned Unix @@ -13,7 +13,7 @@ use std::sync::{LazyLock, Mutex}; use llm_worker::tool::{ToolError, ToolOutput}; use manifest::{AuthRef, ModelManifest, Permission, SchemeKind, ScopeRule}; use pod::runtime::dir::{RuntimeDir, SpawnedPodRecord}; -use pod::runtime::scope_lock::{self, LockFileGuard}; +use pod::runtime::pod_registry::{self, LockFileGuard}; use pod::spawn::registry::SpawnedPodRegistry; use pod::spawn::tool::spawn_pod_tool; use protocol::Method; @@ -40,7 +40,7 @@ impl EnvGuard { } /// Set up a tempdir, point `INSOMNIA_RUNTIME_DIR` at it (so -/// `scope.lock` and per-Pod runtime subdirs both land in the +/// `pods.json` 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 @@ -64,7 +64,7 @@ async fn setup_spawner( .unwrap(); let spawner_socket = spawner_rd.socket_path(); - let _guard = scope_lock::install_top_level( + let _guard = pod_registry::install_top_level( spawner_name.into(), std::process::id(), spawner_socket.clone(), @@ -207,8 +207,8 @@ async fn spawn_pod_delegates_scope_and_sends_run() { other => panic!("expected Run, got {other:?}"), } - // Verify scope_lock has the child allocation under `root`. - let lock_path = scope_lock::default_lock_path().unwrap(); + // Verify pod_registry has the child allocation under `root`. + let lock_path = pod_registry::default_registry_path().unwrap(); let guard = LockFileGuard::open(&lock_path).unwrap(); let child = guard .data() @@ -273,7 +273,7 @@ async fn spawn_pod_rejects_scope_outside_spawner() { } // The spawner's allocation is unchanged; no "child" appeared. - let lock_path = scope_lock::default_lock_path().unwrap(); + let lock_path = pod_registry::default_registry_path().unwrap(); let guard = LockFileGuard::open(&lock_path).unwrap(); assert!(guard.data().find("child").is_none()); @@ -334,7 +334,7 @@ async fn spawn_pod_rolls_back_reservation_when_socket_never_appears() { } // Rollback assertion: the reserved "ghost" allocation is gone. - let lock_path = scope_lock::default_lock_path().unwrap(); + let lock_path = pod_registry::default_registry_path().unwrap(); let guard = LockFileGuard::open(&lock_path).unwrap(); assert!( guard.data().find("ghost").is_none(), diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index 7f9a460e..aec9c5a5 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -39,7 +39,7 @@ pub enum Method { /// /// Delivered as `Method::PodEvent` over the parent's Unix socket. The /// parent Controller applies variant-specific side effects (registry / -/// scope-lock updates) and renders a human-readable string that is +/// pod-registry updates) and renders a human-readable string that is /// injected into the parent's LLM context via the notification buffer. /// /// Transport is fire-and-forget; receivers must tolerate out-of-order diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml index 4e795838..9816ee19 100644 --- a/crates/tui/Cargo.toml +++ b/crates/tui/Cargo.toml @@ -15,4 +15,4 @@ uuid = "1.23" toml = "1.1.2" manifest = { version = "0.1.0", path = "../manifest" } session-store = { version = "0.1.0", path = "../session-store" } -scope-lock = { version = "0.1.0", path = "../scope-lock" } +pod-registry = { version = "0.1.0", path = "../pod-registry" } diff --git a/crates/tui/src/picker.rs b/crates/tui/src/picker.rs index e0726811..c29ca30a 100644 --- a/crates/tui/src/picker.rs +++ b/crates/tui/src/picker.rs @@ -5,7 +5,7 @@ //! `SessionId`. Closes its inline viewport before returning so the //! caller can open a fresh viewport for the name dialog. //! -//! The picker only handles selection. Forking, scope-lock checks, and +//! The picker only handles selection. Forking, pod-registry checks, and //! actual `pod` launch happen later in the resume flow. use std::io; @@ -19,7 +19,7 @@ use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::Paragraph; use ratatui::{Frame, TerminalOptions, Viewport}; -use scope_lock::lookup_session; +use pod_registry::lookup_session; use session_store::{ ContentPart, FsStore, HashedEntry, Item, LogEntry, SessionId, Store, }; @@ -73,7 +73,7 @@ struct Row { /// Last user / assistant snippet, or a `[corrupt]` placeholder. preview: String, /// `Some(pod_name)` when a live Pod currently holds an allocation - /// for this session in `scope.lock`. Picking such a row launches + /// for this session in `pods.json`. Picking such a row launches /// `pod --session ` which will fail with `SessionConflict` — /// the badge warns the user up-front. live_pod: Option, @@ -88,7 +88,7 @@ pub async fn run() -> Result { let mut rows: Vec = Vec::with_capacity(MAX_ROWS); for id in ids.into_iter().take(MAX_ROWS) { let preview = build_preview(&store, id).await; - // Best-effort live check. A scope.lock I/O hiccup downgrades + // Best-effort live check. A pods.json I/O hiccup downgrades // the row to "no badge" rather than killing the picker — the // user still gets to see the listing. let live_pod = lookup_session(id).ok().flatten().map(|info| info.pod_name); diff --git a/tickets/dynamic-scope.md b/tickets/dynamic-scope.md index 1afc7f0c..7be03251 100644 --- a/tickets/dynamic-scope.md +++ b/tickets/dynamic-scope.md @@ -2,9 +2,9 @@ ## 背景 -現状の Pod の `Scope` は `Pod::from_manifest` 時に1回構築され、以後 immutable。scope lock file (`tickets/scope-lock.md`) は登録・削除・衝突チェックを任意のタイミングで行えるため lock file 側の制約はないが、Pod 内部の `Scope` と `ScopedFs` が起動時に固定されているため、実行中に scope を追加・縮小することができない。 +現状の Pod の `Scope` は `Pod::from_manifest` 時に1回構築され、以後 immutable。pod-registry (`crates/pod-registry`) は登録・削除・衝突チェックを任意のタイミングで行えるためレジストリ側の制約はないが、Pod 内部の `Scope` と `ScopedFs` が起動時に固定されているため、実行中に scope を追加・縮小することができない。 -オーケストレーションでは SpawnPod による scope 分譲で effective scope が縮小するが、これは lock file 上の記録にとどまり、Pod 側の `ScopedFs` は元の scope のまま動作している(ツール実行時に lock file と照合していない)。 +オーケストレーションでは SpawnPod による scope 分譲で effective scope が縮小するが、これは pod-registry 上の記録にとどまり、Pod 側の `ScopedFs` は元の scope のまま動作している(ツール実行時に pod-registry と照合していない)。 また将来、外部から Pod に scope を動的に付与するケース(人間が「このディレクトリも触っていいよ」と追加する、別 Pod が scope を委譲してくる等)にも対応したい。 @@ -18,9 +18,9 @@ Pod の実行中に scope を追加・縮小でき、変更が即座にツール - `Scope` を `Arc>`(または同等の共有可変参照)にする - `ScopedFs` が `Scope` の共有参照を持ち、ツール実行時に最新の scope を参照する -- scope 変更メソッド: `pod.update_scope(new_scope_config)` → lock file 更新 + `Scope` 再構築 + `ScopedFs` に反映 +- scope 変更メソッド: `pod.update_scope(new_scope_config)` → pod-registry 更新 + `Scope` 再構築 + `ScopedFs` に反映 -### scope lock file との連携 +### pod-registry との連携 - scope 追加時: `flock → 衝突チェック → 追加分を登録 → unlock → Pod 内 Scope 再構築` - scope 縮小時(分譲): `flock → 分譲を記録 → unlock → Pod 内 Scope 再構築` @@ -36,13 +36,9 @@ Pod の実行中に scope を追加・縮小でき、変更が即座にツール - Pod の実行中に scope を追加でき、追加後のツール実行が新しい scope を反映する - Pod の実行中に scope を縮小でき、縮小後のツール実行が制限を反映する -- scope 変更が lock file と Pod 内 Scope の両方に整合的に反映される +- scope 変更が pod-registry と Pod 内 Scope の両方に整合的に反映される - 単体テストで動的追加・縮小後の permission チェックが検証される -## 依存 - -- `tickets/scope-lock.md`: lock file 基盤 - ## 範囲外 - protocol 経由の外部からの scope 付与 / 剥奪(必要になったら追加)